Implement toggle thinking blocks with localStorage persistence
- Add preferences store to manage user preferences in localStorage - Toggle thinking blocks visibility via command palette (default: hidden) - Dynamic command label shows current state (Show/Hide Thinking Blocks) - Filter reasoning parts based on preference in message-stream - Conditionally render reasoning parts in message-part component - Support function labels in Command interface for dynamic text
This commit is contained in:
@@ -13,6 +13,7 @@ import { initMarkdown } from "./lib/markdown"
|
|||||||
import { createCommandRegistry } from "./lib/commands"
|
import { createCommandRegistry } from "./lib/commands"
|
||||||
import type { Command } from "./lib/commands"
|
import type { Command } from "./lib/commands"
|
||||||
import { hasInstances, isSelectingFolder, setIsSelectingFolder, setHasInstances } from "./stores/ui"
|
import { hasInstances, isSelectingFolder, setIsSelectingFolder, setHasInstances } from "./stores/ui"
|
||||||
|
import { toggleShowThinkingBlocks, preferences } from "./stores/preferences"
|
||||||
import {
|
import {
|
||||||
createInstance,
|
createInstance,
|
||||||
instances,
|
instances,
|
||||||
@@ -588,13 +589,11 @@ const App: Component = () => {
|
|||||||
|
|
||||||
commandRegistry.register({
|
commandRegistry.register({
|
||||||
id: "thinking",
|
id: "thinking",
|
||||||
label: "Toggle Thinking Blocks",
|
label: () => `${preferences().showThinkingBlocks ? "Hide" : "Show"} Thinking Blocks`,
|
||||||
description: "Show/hide AI thinking process",
|
description: "Show/hide AI thinking process",
|
||||||
category: "System",
|
category: "System",
|
||||||
keywords: ["/thinking", "toggle", "show", "hide"],
|
keywords: ["/thinking", "toggle", "show", "hide"],
|
||||||
action: () => {
|
action: toggleShowThinkingBlocks,
|
||||||
console.log("Toggle thinking blocks (not implemented)")
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
commandRegistry.register({
|
commandRegistry.register({
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
|
|||||||
if (!q) return props.commands
|
if (!q) return props.commands
|
||||||
|
|
||||||
return props.commands.filter((cmd) => {
|
return props.commands.filter((cmd) => {
|
||||||
const labelMatch = cmd.label.toLowerCase().includes(q)
|
const label = typeof cmd.label === "function" ? cmd.label() : cmd.label
|
||||||
|
const labelMatch = label.toLowerCase().includes(q)
|
||||||
const descMatch = cmd.description.toLowerCase().includes(q)
|
const descMatch = cmd.description.toLowerCase().includes(q)
|
||||||
const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q))
|
const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q))
|
||||||
const categoryMatch = cmd.category?.toLowerCase().includes(q)
|
const categoryMatch = cmd.category?.toLowerCase().includes(q)
|
||||||
@@ -190,7 +191,9 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
|
|||||||
onMouseEnter={() => setSelectedIndex(commandIndex)}
|
onMouseEnter={() => setSelectedIndex(commandIndex)}
|
||||||
>
|
>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="font-medium text-gray-900 dark:text-gray-100">{command.label}</div>
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{typeof command.label === "function" ? command.label() : command.label}
|
||||||
|
</div>
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">
|
||||||
{command.description}
|
{command.description}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ToolCall from "./tool-call"
|
|||||||
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
|
import { preferences } from "../stores/preferences"
|
||||||
|
|
||||||
interface MessagePartProps {
|
interface MessagePartProps {
|
||||||
part: any
|
part: any
|
||||||
@@ -38,19 +39,21 @@ export default function MessagePart(props: MessagePartProps) {
|
|||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={partType() === "reasoning"}>
|
<Match when={partType() === "reasoning"}>
|
||||||
<div class="message-reasoning">
|
<Show when={preferences().showThinkingBlocks}>
|
||||||
<div class="reasoning-container">
|
<div class="message-reasoning">
|
||||||
<div class="reasoning-header" onClick={handleReasoningClick}>
|
<div class="reasoning-container">
|
||||||
<span class="reasoning-icon">{isReasoningExpanded() ? "▼" : "▶"}</span>
|
<div class="reasoning-header" onClick={handleReasoningClick}>
|
||||||
<span class="reasoning-label">Reasoning</span>
|
<span class="reasoning-icon">{isReasoningExpanded() ? "▼" : "▶"}</span>
|
||||||
</div>
|
<span class="reasoning-label">Reasoning</span>
|
||||||
<Show when={isReasoningExpanded()}>
|
|
||||||
<div class="message-text mt-2">
|
|
||||||
<Markdown content={props.part.text || ""} isDark={isDark()} />
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Show when={isReasoningExpanded()}>
|
||||||
|
<div class="message-text mt-2">
|
||||||
|
<Markdown content={props.part.text || ""} isDark={isDark()} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import MessageItem from "./message-item"
|
|||||||
import ToolCall from "./tool-call"
|
import ToolCall from "./tool-call"
|
||||||
import { sseManager } from "../lib/sse-manager"
|
import { sseManager } from "../lib/sse-manager"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
|
import { preferences } from "../stores/preferences"
|
||||||
|
|
||||||
interface MessageStreamProps {
|
interface MessageStreamProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
@@ -72,7 +73,7 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
|
|
||||||
const textParts = message.parts.filter((p) => p.type === "text" && !p.synthetic)
|
const textParts = message.parts.filter((p) => p.type === "text" && !p.synthetic)
|
||||||
const toolParts = message.parts.filter((p) => p.type === "tool")
|
const toolParts = message.parts.filter((p) => p.type === "tool")
|
||||||
const reasoningParts = message.parts.filter((p) => p.type === "reasoning")
|
const reasoningParts = preferences().showThinkingBlocks ? message.parts.filter((p) => p.type === "reasoning") : []
|
||||||
|
|
||||||
const isQueued = message.type === "user" && message.id > lastAssistantMessageId
|
const isQueued = message.type === "user" && message.id > lastAssistantMessageId
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface KeyboardShortcut {
|
|||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string | (() => string)
|
||||||
description: string
|
description: string
|
||||||
keywords?: string[]
|
keywords?: string[]
|
||||||
shortcut?: KeyboardShortcut
|
shortcut?: KeyboardShortcut
|
||||||
@@ -47,7 +47,8 @@ export function createCommandRegistry() {
|
|||||||
|
|
||||||
const lowerQuery = query.toLowerCase()
|
const lowerQuery = query.toLowerCase()
|
||||||
return getAll().filter((cmd) => {
|
return getAll().filter((cmd) => {
|
||||||
const labelMatch = cmd.label.toLowerCase().includes(lowerQuery)
|
const label = typeof cmd.label === "function" ? cmd.label() : cmd.label
|
||||||
|
const labelMatch = label.toLowerCase().includes(lowerQuery)
|
||||||
const descMatch = cmd.description.toLowerCase().includes(lowerQuery)
|
const descMatch = cmd.description.toLowerCase().includes(lowerQuery)
|
||||||
const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(lowerQuery))
|
const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(lowerQuery))
|
||||||
return labelMatch || descMatch || keywordMatch
|
return labelMatch || descMatch || keywordMatch
|
||||||
|
|||||||
45
src/stores/preferences.ts
Normal file
45
src/stores/preferences.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { createSignal } from "solid-js"
|
||||||
|
|
||||||
|
const STORAGE_KEY = "opencode-preferences"
|
||||||
|
|
||||||
|
interface Preferences {
|
||||||
|
showThinkingBlocks: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPreferences: Preferences = {
|
||||||
|
showThinkingBlocks: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPreferences(): Preferences {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
return { ...defaultPreferences, ...JSON.parse(stored) }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load preferences:", error)
|
||||||
|
}
|
||||||
|
return defaultPreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreferences(prefs: Preferences): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save preferences:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [preferences, setPreferences] = createSignal<Preferences>(loadPreferences())
|
||||||
|
|
||||||
|
function updatePreferences(updates: Partial<Preferences>): void {
|
||||||
|
const updated = { ...preferences(), ...updates }
|
||||||
|
setPreferences(updated)
|
||||||
|
savePreferences(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleShowThinkingBlocks(): void {
|
||||||
|
updatePreferences({ showThinkingBlocks: !preferences().showThinkingBlocks })
|
||||||
|
}
|
||||||
|
|
||||||
|
export { preferences, updatePreferences, toggleShowThinkingBlocks }
|
||||||
Reference in New Issue
Block a user