diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 61c15ebad..343ca718f 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -22,6 +22,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach import { getPartCharCount } from "../lib/token-utils" import { buildSessionSearchMatches } from "../lib/session-search" import type { SessionSearchMatch } from "../lib/session-search" +import { resolveThinkingExpansionDefault } from "./tool-call/tool-registry" const SCROLL_SENTINEL_MARGIN_PX = 8 const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream" @@ -117,7 +118,7 @@ export default function MessageSection(props: MessageSectionProps) { const preferenceSignature = createMemo(() => { const pref = preferences() const showThinking = pref.showThinkingBlocks ? 1 : 0 - const thinkingExpansion = pref.thinkingBlocksExpansion ?? "expanded" + const thinkingExpansion = resolveThinkingExpansionDefault(pref) ? "expanded" : "collapsed" const showUsage = (pref.showUsageMetrics ?? true) ? 1 : 0 return `${showThinking}|${thinkingExpansion}|${showUsage}` }) @@ -1495,7 +1496,7 @@ export default function MessageSection(props: MessageSectionProps) { messageIndex={index} lastAssistantIndex={lastAssistantIndex} showThinking={() => preferences().showThinkingBlocks} - thinkingDefaultExpanded={() => (preferences().thinkingBlocksExpansion ?? "expanded") === "expanded"} + thinkingDefaultExpanded={() => resolveThinkingExpansionDefault(preferences())} showUsageMetrics={showUsagePreference} deleteHover={deleteHover} onDeleteHoverChange={setDeleteHover} diff --git a/packages/ui/src/components/settings/appearance-settings-section.tsx b/packages/ui/src/components/settings/appearance-settings-section.tsx index cfb039817..ea3172de5 100644 --- a/packages/ui/src/components/settings/appearance-settings-section.tsx +++ b/packages/ui/src/components/settings/appearance-settings-section.tsx @@ -3,8 +3,14 @@ import { createEffect, createMemo, createSignal, For, type Component } from "sol import { Check, ChevronDown, Laptop, Moon, Sun } from "lucide-solid" import { useI18n } from "../../lib/i18n" import { useTheme, type ThemeMode } from "../../lib/theme" -import { useConfig } from "../../stores/preferences" +import { useConfig, type ExpansionPreference, type ToolCallExpansionPreset } from "../../stores/preferences" import { getBehaviorSettings, type BehaviorSetting } from "../../lib/settings/behavior-registry" +import { + buildToolExpansionPresetDefaults, + getConfigurableToolEntries, + OTHER_TOOL_NAME, + THINKING_EXPANSION_PRESETS, +} from "../tool-call/tool-registry" const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [ { value: "system", icon: Laptop }, @@ -12,6 +18,8 @@ const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [ { value: "dark", icon: Moon }, ] +const toolExpansionPresetOptions: ToolCallExpansionPreset[] = ["minimal", "balanced", "detailed", "everything"] + export const AppearanceSettingsSection: Component = () => { const { t } = useI18n() const { themeMode, setThemeMode } = useTheme() @@ -45,16 +53,18 @@ export const AppearanceSettingsSection: Component = () => { toggleKeyboardShortcutHints, toggleShowMessageTimeline, toggleShowTimelineTools, - toggleUsageMetrics, - toggleAutoCleanupBlankSessions, - togglePromptSubmitOnEnter, - toggleShowPromptVoiceInput, - setDiffViewMode, + toggleUsageMetrics, + toggleAutoCleanupBlankSessions, + togglePromptSubmitOnEnter, + toggleShowPromptVoiceInput, + setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, setThinkingBlocksExpansion, setToolInputsVisibility, - }), + }).filter( + (setting) => setting.id !== "behavior.thinkingBlocksDefault" && setting.id !== "behavior.toolOutputsDefault", + ), ) const [overrides, setOverrides] = createSignal>(new Map()) @@ -99,6 +109,78 @@ export const AppearanceSettingsSection: Component = () => { type SelectOption = { value: string; label: string } + type ExpansionRow = + | { kind: "thinking"; key: "thinking"; label: string } + | { kind: "tool"; key: string; label: string } + + const expansionOptions = createMemo(() => [ + { value: "collapsed", label: t("commands.common.collapsed") }, + { value: "expanded", label: t("commands.common.expanded") }, + ]) + + const toolExpansionRows = createMemo(() => [ + { kind: "thinking", key: "thinking", label: t("settings.behavior.expansionDefaults.thinking") }, + ...getConfigurableToolEntries().map((entry) => ({ + kind: "tool" as const, + key: entry.tool, + label: entry.labelKey ? t(entry.labelKey) : entry.label, + })), + ]) + + const currentPreset = createMemo(() => preferences().toolCallExpansionDefaults.preset) + + const currentToolMode = (tool: string): ExpansionPreference => { + const pref = preferences().toolCallExpansionDefaults + const entry = getConfigurableToolEntries().find((item) => item.tool === tool) + if (pref.tools[tool]) return pref.tools[tool] + if (pref.preset !== "custom" && entry) return entry.expansionPresets[pref.preset] + return pref.tools[OTHER_TOOL_NAME] ?? "expanded" + } + + const currentThinkingMode = (): ExpansionPreference => { + const pref = preferences().toolCallExpansionDefaults + if (pref.thinking) return pref.thinking + if (pref.preset !== "custom") return THINKING_EXPANSION_PRESETS[pref.preset] + return preferences().thinkingBlocksExpansion ?? "expanded" + } + + const materializeToolModes = () => { + const tools: Record = {} + for (const entry of getConfigurableToolEntries()) { + tools[entry.tool] = currentToolMode(entry.tool) + } + return tools + } + + const applyExpansionPreset = (preset: ToolCallExpansionPreset) => { + const tools = buildToolExpansionPresetDefaults(preset) + const thinking = THINKING_EXPANSION_PRESETS[preset] + updatePreferences({ + toolCallExpansionDefaults: { preset, thinking, tools }, + thinkingBlocksExpansion: thinking, + toolOutputExpansion: tools[OTHER_TOOL_NAME] ?? "expanded", + }) + } + + const setExpansionRowMode = (row: ExpansionRow, mode: ExpansionPreference) => { + const tools = materializeToolModes() + const thinking = row.kind === "thinking" ? mode : currentThinkingMode() + if (row.kind === "tool") { + tools[row.key] = mode + } + updatePreferences({ + toolCallExpansionDefaults: { preset: "custom", thinking, tools }, + thinkingBlocksExpansion: thinking, + toolOutputExpansion: tools[OTHER_TOOL_NAME] ?? preferences().toolOutputExpansion, + }) + } + + const rowMode = (row: ExpansionRow): ExpansionPreference => + row.kind === "thinking" ? currentThinkingMode() : currentToolMode(row.key) + + const selectedExpansionOption = (mode: ExpansionPreference) => + expansionOptions().find((opt) => opt.value === mode) + const BehaviorRow: Component<{ setting: BehaviorSetting }> = (props) => { const setting = props.setting const disabled = createMemo(() => (setting.disabled ? Boolean(setting.disabled()) : false)) @@ -224,6 +306,8 @@ export const AppearanceSettingsSection: Component = () => { return t("theme.mode.dark") } + const presetLabel = (preset: ToolCallExpansionPreset | "custom") => t(`settings.behavior.expansionPreset.${preset}.title`) + return (
@@ -266,7 +350,80 @@ export const AppearanceSettingsSection: Component = () => {

{t("settings.appearance.behavior.title")}

{t("settings.appearance.behavior.subtitle")}

- {t("settings.scope.device")} + {presetLabel(currentPreset())} +
+ +
+ + {(preset) => ( + + )} + +
+ +
+
+ {t("settings.behavior.expansionDefaults.itemColumn")} + {t("settings.behavior.expansionDefaults.stateColumn")} +
+ + {(row) => { + const selected = createMemo(() => selectedExpansionOption(rowMode(row))) + return ( +
+
+ {row.label} +
+
+ + value={selected()} + onChange={(opt) => { + if (!opt) return + setExpansionRowMode(row, opt.value as ExpansionPreference) + }} + options={expansionOptions()} + optionValue="value" + optionTextValue="label" + itemComponent={(itemProps) => ( + + {itemProps.item.rawValue.label} + + )} + > + +
+ > + {(state) => ( + + {state.selectedOption()?.label} + + )} + +
+ + + +
+ + + + + + + +
+
+ ) + }} +
diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index d54bd9868..a042156e3 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -12,6 +12,7 @@ import { getPermissionSessionId } from "../types/permission" import type { QuestionRequest } from "../types/question" import { useI18n } from "../lib/i18n" import { resolveToolRenderer } from "./tool-call/renderers" +import { resolveToolExpansionDefault } from "./tool-call/tool-registry" import { QuestionToolBlock } from "./tool-call/question-block" import { PermissionToolBlock } from "./tool-call/permission-block" import { createAnsiContentRenderer } from "./tool-call/ansi-render" @@ -40,7 +41,6 @@ import { getDefaultToolAction, readToolStatePayload, } from "./tool-call/utils" -import { resolveTitleForTool } from "./tool-call/tool-title" import { getLogger } from "../lib/logger" import { useSpeech } from "../lib/hooks/use-speech" import { createFollowScroll } from "../lib/follow-scroll" @@ -514,7 +514,7 @@ function ToolCallDetails(props: { const shouldShowPendingMessage = () => { const tool = props.toolName() - return status() === "pending" && !props.pendingPermission() && tool !== "todowrite" && tool !== "todoread" + return status() === "pending" && !props.pendingPermission() && tool !== "todowrite" } const copyIoText = async (event: MouseEvent, text?: string | null) => { @@ -746,23 +746,17 @@ export default function ToolCall(props: ToolCallProps) { return undefined }) - const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "expanded") === "expanded") const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded") const defaultExpandedForTool = createMemo(() => { if (props.forceCollapsed) { return false } - const prefExpanded = toolOutputDefaultExpanded() - const toolName = toolCallMemo()?.tool || "" - if (toolName === "read" || toolName === "skill") { - const state = toolState() - if (state?.status === "error") { - return true - } - return false + const state = toolState() + if (state?.status === "error") { + return true } - return prefExpanded + return resolveToolExpansionDefault(preferences(), toolCallMemo()?.tool || "") }) const [userExpanded, setUserExpanded] = createSignal(null) @@ -911,7 +905,17 @@ export default function ToolCall(props: ToolCallProps) { const currentTool = toolName() if (currentTool !== "task") { - return resolveTitleForTool({ toolName: currentTool, state }) + if (!state || state.status === "pending") return getRendererAction() + + const stateTitle = typeof (state as { title?: string }).title === "string" ? (state as { title?: string }).title : undefined + if (stateTitle && stateTitle.length > 0) { + return stateTitle + } + + const customTitle = renderer().getTitle?.(headerRendererContext) + if (customTitle) return customTitle + + return getToolName(currentTool) } if (!state) return getRendererAction() diff --git a/packages/ui/src/components/tool-call/renderers/index.ts b/packages/ui/src/components/tool-call/renderers/index.ts index 366e9592d..e035910e3 100644 --- a/packages/ui/src/components/tool-call/renderers/index.ts +++ b/packages/ui/src/components/tool-call/renderers/index.ts @@ -1,44 +1,2 @@ -import type { ToolRenderer } from "../types" -import { bashRenderer } from "./bash" -import { defaultRenderer } from "./default" -import { editRenderer } from "./edit" -import { applyPatchRenderer } from "./apply-patch" -import { patchRenderer } from "./patch" -import { readRenderer } from "./read" -import { skillRenderer } from "./skill" -import { taskRenderer } from "./task" -import { todoRenderer } from "./todo" -import { webfetchRenderer } from "./webfetch" -import { writeRenderer } from "./write" -import { invalidRenderer } from "./invalid" -import { questionRenderer } from "./question" -import { searchRenderer } from "./search" - -const TOOL_RENDERERS: ToolRenderer[] = [ - bashRenderer, - skillRenderer, - readRenderer, - writeRenderer, - editRenderer, - applyPatchRenderer, - patchRenderer, - webfetchRenderer, - searchRenderer, - todoRenderer, - taskRenderer, - questionRenderer, - invalidRenderer, -] - -const rendererMap = TOOL_RENDERERS.reduce>((acc, renderer) => { - renderer.tools.forEach((tool) => { - acc[tool] = renderer - }) - return acc -}, {}) - -export function resolveToolRenderer(toolName: string): ToolRenderer { - return rendererMap[toolName] ?? defaultRenderer -} - -export { defaultRenderer } +export { resolveToolRenderer } from "../tool-registry" +export { defaultRenderer } from "./default" diff --git a/packages/ui/src/components/tool-call/renderers/task.tsx b/packages/ui/src/components/tool-call/renderers/task.tsx index bbbb531fe..fce9b3781 100644 --- a/packages/ui/src/components/tool-call/renderers/task.tsx +++ b/packages/ui/src/components/tool-call/renderers/task.tsx @@ -2,7 +2,6 @@ import { For, Index, Show, createEffect, createMemo, createSignal, untrack } fro import type { ToolState } from "@opencode-ai/sdk/v2" import type { ToolRenderer } from "../types" import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils" -import { resolveTitleForTool } from "../tool-title" import { messageStoreBus } from "../../../stores/message-v2/bus" import { loadMessages } from "../../../stores/session-api" import { loading, messagesLoaded } from "../../../stores/session-state" @@ -121,6 +120,24 @@ function describeTaskTitle(input: Record) { return base } +function describeGenericToolTitle(tool: string, input: Record) { + const base = getToolName(tool) + const detail = + typeof input.description === "string" && input.description.trim().length > 0 + ? input.description.trim() + : typeof input.filePath === "string" && input.filePath.trim().length > 0 + ? input.filePath.trim() + : typeof input.path === "string" && input.path.trim().length > 0 + ? input.path.trim() + : typeof input.url === "string" && input.url.trim().length > 0 + ? input.url.trim() + : typeof input.pattern === "string" && input.pattern.trim().length > 0 + ? input.pattern.trim() + : "" + + return detail ? `${base} ${detail}` : base +} + function describeToolTitle(item: TaskSummaryItem): string { if (item.title && item.title.length > 0) { return item.title @@ -131,7 +148,12 @@ function describeToolTitle(item: TaskSummaryItem): string { } if (item.state) { - return resolveTitleForTool({ toolName: item.tool, state: item.state }) + const stateTitle = typeof (item.state as { title?: string }).title === "string" ? (item.state as { title?: string }).title : undefined + if (stateTitle && stateTitle.length > 0) { + return stateTitle + } + const { input } = readToolStatePayload(item.state) + return describeGenericToolTitle(item.tool, { ...item.metadata, ...item.input, ...input }) } return getDefaultToolAction(item.tool) diff --git a/packages/ui/src/components/tool-call/renderers/todo.tsx b/packages/ui/src/components/tool-call/renderers/todo.tsx index bb906629c..6a8e67060 100644 --- a/packages/ui/src/components/tool-call/renderers/todo.tsx +++ b/packages/ui/src/components/tool-call/renderers/todo.tsx @@ -137,7 +137,7 @@ export function getTodoTitle(state?: ToolState): string { } export const todoRenderer: ToolRenderer = { - tools: ["todowrite", "todoread"], + tools: ["todowrite"], getSearchText: getTodoToolSearchText, getAction: () => tGlobal("toolCall.renderer.action.planning"), getTitle({ toolState }) { diff --git a/packages/ui/src/components/tool-call/tool-registry.ts b/packages/ui/src/components/tool-call/tool-registry.ts new file mode 100644 index 000000000..2358e129d --- /dev/null +++ b/packages/ui/src/components/tool-call/tool-registry.ts @@ -0,0 +1,204 @@ +import type { ExpansionPreference, Preferences, ToolCallExpansionPreset } from "../../stores/preferences" +import type { ToolRenderer } from "./types" +import { applyPatchRenderer } from "./renderers/apply-patch" +import { bashRenderer } from "./renderers/bash" +import { defaultRenderer } from "./renderers/default" +import { editRenderer } from "./renderers/edit" +import { invalidRenderer } from "./renderers/invalid" +import { patchRenderer } from "./renderers/patch" +import { questionRenderer } from "./renderers/question" +import { readRenderer } from "./renderers/read" +import { searchRenderer } from "./renderers/search" +import { skillRenderer } from "./renderers/skill" +import { taskRenderer } from "./renderers/task" +import { todoRenderer } from "./renderers/todo" +import { webfetchRenderer } from "./renderers/webfetch" +import { writeRenderer } from "./renderers/write" + +export const OTHER_TOOL_NAME = "other" + +export const THINKING_EXPANSION_PRESETS: Record = { + minimal: "collapsed", + balanced: "collapsed", + detailed: "expanded", + everything: "expanded", +} + +export interface ToolRegistryEntry { + tool: string + label: string + labelKey?: string + renderer: ToolRenderer + configurable: boolean + expansionPresets: Record + aliases?: string[] +} + +const expanded = "expanded" satisfies ExpansionPreference +const collapsed = "collapsed" satisfies ExpansionPreference + +function presets(values: Record) { + return values +} + +export const TOOL_REGISTRY: ToolRegistryEntry[] = [ + { + tool: "bash", + label: "bash", + renderer: bashRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "read", + label: "read", + renderer: readRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: collapsed, everything: expanded }), + }, + { + tool: "write", + label: "write", + renderer: writeRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "edit", + label: "edit", + renderer: editRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "patch", + label: "patch", + renderer: patchRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "apply_patch", + label: "apply_patch", + renderer: applyPatchRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "webfetch", + label: "webfetch", + renderer: webfetchRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }), + }, + { + tool: "glob", + label: "glob", + renderer: searchRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }), + }, + { + tool: "grep", + label: "grep", + renderer: searchRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }), + }, + { + tool: "todowrite", + label: "todowrite", + renderer: todoRenderer, + configurable: true, + expansionPresets: presets({ minimal: expanded, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "task", + label: "task", + renderer: taskRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "skill", + label: "skill", + renderer: skillRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: collapsed, everything: expanded }), + }, + { + tool: "question", + label: "question", + renderer: questionRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: expanded, detailed: expanded, everything: expanded }), + }, + { + tool: "invalid", + label: "invalid", + renderer: invalidRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: collapsed, everything: expanded }), + }, + { + tool: OTHER_TOOL_NAME, + label: "Other tools", + labelKey: "settings.behavior.expansionDefaults.otherTools", + renderer: defaultRenderer, + configurable: true, + expansionPresets: presets({ minimal: collapsed, balanced: collapsed, detailed: expanded, everything: expanded }), + }, +] + +const otherToolEntry = TOOL_REGISTRY.find((entry) => entry.tool === OTHER_TOOL_NAME)! + +const registryMap = TOOL_REGISTRY.reduce>((acc, entry) => { + acc[entry.tool] = entry + entry.aliases?.forEach((alias) => { + acc[alias] = entry + }) + return acc +}, {}) + +export function getToolRegistryEntry(toolName: string): ToolRegistryEntry { + return registryMap[toolName] ?? otherToolEntry +} + +export function resolveToolRenderer(toolName: string): ToolRenderer { + return getToolRegistryEntry(toolName).renderer +} + +export function getConfigurableToolEntries(): ToolRegistryEntry[] { + return TOOL_REGISTRY.filter((entry) => entry.configurable) +} + +export function buildToolExpansionPresetDefaults(preset: ToolCallExpansionPreset): Record { + const defaults: Record = {} + for (const entry of getConfigurableToolEntries()) { + defaults[entry.tool] = entry.expansionPresets[preset] + } + return defaults +} + +export function resolveToolExpansionDefault(preferences: Preferences, toolName: string): boolean { + const entry = getToolRegistryEntry(toolName) + const defaults = preferences.toolCallExpansionDefaults + const presetMode = defaults.preset === "custom" ? undefined : entry.expansionPresets[defaults.preset] + const otherPresetMode = defaults.preset === "custom" ? undefined : otherToolEntry.expansionPresets[defaults.preset] + const mode = defaults.tools[entry.tool] + ?? presetMode + ?? defaults.tools[OTHER_TOOL_NAME] + ?? otherPresetMode + ?? preferences.toolOutputExpansion + ?? "expanded" + return mode === "expanded" +} + +export function resolveThinkingExpansionDefault(preferences: Preferences): boolean { + const defaults = preferences.toolCallExpansionDefaults + const mode = defaults.thinking + ?? (defaults.preset === "custom" ? undefined : THINKING_EXPANSION_PRESETS[defaults.preset]) + ?? preferences.thinkingBlocksExpansion + ?? "expanded" + return mode === "expanded" +} diff --git a/packages/ui/src/components/tool-call/tool-title.ts b/packages/ui/src/components/tool-call/tool-title.ts deleted file mode 100644 index 1bae85264..000000000 --- a/packages/ui/src/components/tool-call/tool-title.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { ToolState } from "@opencode-ai/sdk/v2" -import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types" -import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils" -import { enMessages } from "../../lib/i18n/messages/en" -import { defaultRenderer } from "./renderers/default" -import { bashRenderer } from "./renderers/bash" -import { readRenderer } from "./renderers/read" -import { skillRenderer } from "./renderers/skill" -import { writeRenderer } from "./renderers/write" -import { editRenderer } from "./renderers/edit" -import { applyPatchRenderer } from "./renderers/apply-patch" -import { patchRenderer } from "./renderers/patch" -import { webfetchRenderer } from "./renderers/webfetch" -import { todoRenderer } from "./renderers/todo" -import { invalidRenderer } from "./renderers/invalid" -import { searchRenderer } from "./renderers/search" - -const TITLE_RENDERERS: Record = { - bash: bashRenderer, - read: readRenderer, - skill: skillRenderer, - write: writeRenderer, - edit: editRenderer, - apply_patch: applyPatchRenderer, - patch: patchRenderer, - webfetch: webfetchRenderer, - glob: searchRenderer, - grep: searchRenderer, - todowrite: todoRenderer, - todoread: todoRenderer, - invalid: invalidRenderer, -} - -interface TitleSnapshot { - toolName: string - state?: ToolState -} - -function lookupRenderer(toolName: string): ToolRenderer { - return TITLE_RENDERERS[toolName] ?? defaultRenderer -} - -function createStaticToolPart(snapshot: TitleSnapshot): ToolCallPart { - return { - id: "", - type: "tool", - tool: snapshot.toolName, - state: snapshot.state, - } as ToolCallPart -} - -function interpolate(template: string, params?: Record): string { - if (!params) return template - return template.replace(/\{(\w+)\}/g, (_match, key: string) => { - const value = params[key] - return value === undefined || value === null ? "" : String(value) - }) -} - -function createStaticT(): ToolRendererContext["t"] { - return (key, params) => { - const template = (enMessages as Record)[key] ?? key - return interpolate(template, params) - } -} - -function createStaticContext(snapshot: TitleSnapshot): ToolRendererContext { - const toolStateAccessor = () => snapshot.state - const toolNameAccessor = () => snapshot.toolName - const toolCallAccessor = () => createStaticToolPart(snapshot) - const messageVersionAccessor = () => undefined - const partVersionAccessor = () => undefined - const t = createStaticT() - const renderMarkdown: ToolRendererContext["renderMarkdown"] = () => null - const renderAnsi: ToolRendererContext["renderAnsi"] = () => null - const renderDiff: ToolRendererContext["renderDiff"] = () => null - - return { - toolCall: toolCallAccessor, - toolState: toolStateAccessor, - toolName: toolNameAccessor, - instanceId: "", - sessionId: "", - t, - messageVersion: messageVersionAccessor, - partVersion: partVersionAccessor, - renderMarkdown, - renderAnsi, - renderDiff, - renderToolCall: () => null, - scrollHelpers: undefined, - } -} - -export function resolveTitleForTool(snapshot: TitleSnapshot): string { - const renderer = lookupRenderer(snapshot.toolName) - const context = createStaticContext(snapshot) - const state = snapshot.state - const defaultAction = renderer.getAction?.(context) ?? getDefaultToolAction(snapshot.toolName) - - if (!state || state.status === "pending") { - return defaultAction - } - - const stateTitle = typeof (state as { title?: string }).title === "string" ? (state as { title?: string }).title : undefined - if (stateTitle && stateTitle.length > 0) { - return stateTitle - } - - const customTitle = renderer.getTitle?.(context) - if (customTitle) { - return customTitle - } - - return getToolName(snapshot.toolName) -} diff --git a/packages/ui/src/components/tool-call/utils.ts b/packages/ui/src/components/tool-call/utils.ts index 766d8d998..1ea469b5e 100644 --- a/packages/ui/src/components/tool-call/utils.ts +++ b/packages/ui/src/components/tool-call/utils.ts @@ -44,7 +44,6 @@ export function getToolIcon(tool: string): string { case "task": return "🎯" case "todowrite": - case "todoread": return "📋" case "question": return "❓" @@ -68,7 +67,6 @@ export function getToolName(tool: string): string { case "invalid": return tGlobal("toolCall.renderer.toolName.invalid") case "todowrite": - case "todoread": return tGlobal("toolCall.renderer.toolName.plan") case "apply_patch": return tGlobal("toolCall.renderer.toolName.applyPatch") @@ -221,7 +219,6 @@ export function getDefaultToolAction(toolName: string) { case "write": return tGlobal("toolCall.renderer.action.preparingWrite") case "todowrite": - case "todoread": return tGlobal("toolCall.renderer.action.planning") case "patch": return tGlobal("toolCall.renderer.action.preparingPatch") diff --git a/packages/ui/src/lib/i18n/messages/de/settings.ts b/packages/ui/src/lib/i18n/messages/de/settings.ts index f656a9f60..e247fe3d7 100644 --- a/packages/ui/src/lib/i18n/messages/de/settings.ts +++ b/packages/ui/src/lib/i18n/messages/de/settings.ts @@ -210,6 +210,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Interaktion", "settings.appearance.behavior.subtitle": "Standardwerte für Nachrichten, Diffs und Eingaben.", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "Tastenkombinations-Hinweise", "settings.behavior.keyboardHints.subtitle": "Tastenkombinations-Hinweise in der UI anzeigen.", "settings.behavior.thinking.title": "Denkabschnitte", diff --git a/packages/ui/src/lib/i18n/messages/en/settings.ts b/packages/ui/src/lib/i18n/messages/en/settings.ts index 9a58dabc7..fc408b69a 100644 --- a/packages/ui/src/lib/i18n/messages/en/settings.ts +++ b/packages/ui/src/lib/i18n/messages/en/settings.ts @@ -210,6 +210,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Interaction", "settings.appearance.behavior.subtitle": "Message, diff, and input defaults.", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "Keyboard shortcut hints", "settings.behavior.keyboardHints.subtitle": "Show keyboard shortcut hints across the UI.", "settings.behavior.thinking.title": "Thinking sections", diff --git a/packages/ui/src/lib/i18n/messages/es/settings.ts b/packages/ui/src/lib/i18n/messages/es/settings.ts index 2f7283f93..1e9a3098d 100644 --- a/packages/ui/src/lib/i18n/messages/es/settings.ts +++ b/packages/ui/src/lib/i18n/messages/es/settings.ts @@ -209,6 +209,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Interacción", "settings.appearance.behavior.subtitle": "Valores predeterminados de mensajes, diffs y entrada.", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "Sugerencias de atajos de teclado", "settings.behavior.keyboardHints.subtitle": "Muestra sugerencias de atajos de teclado en toda la interfaz.", "settings.behavior.thinking.title": "Secciones de pensamiento", diff --git a/packages/ui/src/lib/i18n/messages/fr/settings.ts b/packages/ui/src/lib/i18n/messages/fr/settings.ts index abb319e0c..a88789390 100644 --- a/packages/ui/src/lib/i18n/messages/fr/settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr/settings.ts @@ -209,6 +209,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Interaction", "settings.appearance.behavior.subtitle": "Parametres par defaut pour les messages, les diffs et la saisie.", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "Indications de raccourcis clavier", "settings.behavior.keyboardHints.subtitle": "Afficher des indications de raccourcis clavier dans toute l'interface.", "settings.behavior.thinking.title": "Sections de reflexion", diff --git a/packages/ui/src/lib/i18n/messages/he/settings.ts b/packages/ui/src/lib/i18n/messages/he/settings.ts index e9961c7f9..d5f61bb7e 100644 --- a/packages/ui/src/lib/i18n/messages/he/settings.ts +++ b/packages/ui/src/lib/i18n/messages/he/settings.ts @@ -209,6 +209,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "אינטראקציה", "settings.appearance.behavior.subtitle": "ברירות מחדל להודעות, diff וקלט.", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "רמזי קיצורי מקלדת", "settings.behavior.keyboardHints.subtitle": "הצג רמזי קיצורי מקלדת בכל הממשק.", "settings.behavior.thinking.title": "קטעי חשיבה", diff --git a/packages/ui/src/lib/i18n/messages/ja/settings.ts b/packages/ui/src/lib/i18n/messages/ja/settings.ts index 940339964..1bf5ae7b0 100644 --- a/packages/ui/src/lib/i18n/messages/ja/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja/settings.ts @@ -209,6 +209,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "操作", "settings.appearance.behavior.subtitle": "メッセージ、差分、入力の既定値。", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "キーボードショートカットのヒント", "settings.behavior.keyboardHints.subtitle": "UI全体でキーボードショートカットのヒントを表示します。", "settings.behavior.thinking.title": "思考セクション", diff --git a/packages/ui/src/lib/i18n/messages/ne/settings.ts b/packages/ui/src/lib/i18n/messages/ne/settings.ts index 83e022ac7..0e474be7e 100644 --- a/packages/ui/src/lib/i18n/messages/ne/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ne/settings.ts @@ -210,6 +210,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "अन्तरक्रिया", "settings.appearance.behavior.subtitle": "सन्देश, डिफ र इनपुट पूर्वनिर्धारितहरू।", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "किबोर्ड सर्टकट संकेतहरू", "settings.behavior.keyboardHints.subtitle": "UI मा किबोर्ड सर्टकट संकेतहरू देखाउनुहोस्।", "settings.behavior.thinking.title": "सोचाइ खण्डहरू", diff --git a/packages/ui/src/lib/i18n/messages/ru/settings.ts b/packages/ui/src/lib/i18n/messages/ru/settings.ts index 5cf164078..665f3e04f 100644 --- a/packages/ui/src/lib/i18n/messages/ru/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ru/settings.ts @@ -209,6 +209,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Взаимодействие", "settings.appearance.behavior.subtitle": "Значения по умолчанию для сообщений, диффов и ввода.", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "Подсказки сочетаний клавиш", "settings.behavior.keyboardHints.subtitle": "Показывать подсказки сочетаний клавиш по всему интерфейсу.", "settings.behavior.thinking.title": "Разделы размышлений", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts index a81b37623..fc9706c72 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts @@ -209,6 +209,23 @@ export const settingsMessages = { "settings.appearance.behavior.title": "交互", "settings.appearance.behavior.subtitle": "消息、差异与输入的默认值。", + "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.behavior.expansionPreset.minimal.title": "Minimal", + "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", + "settings.behavior.expansionPreset.balanced.title": "Balanced", + "settings.behavior.expansionPreset.balanced.description": "Show important actions while keeping noisy reads and fetches compact.", + "settings.behavior.expansionPreset.detailed.title": "Detailed", + "settings.behavior.expansionPreset.detailed.description": "Open most tool and thinking output for a fuller transcript.", + "settings.behavior.expansionPreset.everything.title": "Everything", + "settings.behavior.expansionPreset.everything.description": "Expand all tool and thinking output by default.", + "settings.behavior.expansionPreset.custom.title": "Custom", + "settings.behavior.expansionPreset.custom.description": "Your customized transcript detail defaults.", + "settings.behavior.expansionDefaults.title": "Expansion defaults", + "settings.behavior.expansionDefaults.itemColumn": "Item", + "settings.behavior.expansionDefaults.stateColumn": "Default state", + "settings.behavior.expansionDefaults.thinking": "thinking", + "settings.behavior.expansionDefaults.otherTools": "Other tools", + "settings.behavior.expansionDefaults.rowAriaLabel": "Default state for {item}", "settings.behavior.keyboardHints.title": "键盘快捷键提示", "settings.behavior.keyboardHints.subtitle": "在整个界面中显示键盘快捷键提示。", "settings.behavior.thinking.title": "思考区块", diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index 3f1f88ee0..e3fde8945 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -31,6 +31,8 @@ export interface ModelPreference { export type DiffViewMode = "split" | "unified" export type ExpansionPreference = "expanded" | "collapsed" +export type ToolCallExpansionPreset = "minimal" | "balanced" | "detailed" | "everything" +export type ToolCallExpansionPresetSelection = ToolCallExpansionPreset | "custom" export type ToolInputsVisibilityPreference = "hidden" | "collapsed" | "expanded" export type ListeningMode = "local" | "all" export type ServerLogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" @@ -38,6 +40,12 @@ export type SpeechProviderPreference = "openai-compatible" export type SpeechPlaybackMode = "streaming" | "buffered" export type SpeechTtsFormat = "mp3" | "wav" | "opus" | "aac" +export interface ToolCallExpansionDefaults { + preset: ToolCallExpansionPresetSelection + thinking: ExpansionPreference + tools: Record +} + export interface SpeechSettings { provider: SpeechProviderPreference apiKey?: string @@ -65,6 +73,7 @@ export interface UiSettings { showPromptVoiceInput: boolean locale?: string diffViewMode: DiffViewMode + toolCallExpansionDefaults: ToolCallExpansionDefaults toolOutputExpansion: ExpansionPreference diagnosticsExpansion: ExpansionPreference toolInputsVisibility: ToolInputsVisibilityPreference @@ -137,16 +146,23 @@ const MAX_RECENT_FOLDERS = 20 const MAX_RECENT_MODELS = 5 const MAX_FAVORITE_MODELS = 50 +const defaultToolCallExpansionDefaults: ToolCallExpansionDefaults = { + preset: "balanced", + thinking: "collapsed", + tools: {}, +} + const defaultUiSettings: UiSettings = { showThinkingBlocks: false, showKeyboardShortcutHints: true, - thinkingBlocksExpansion: "expanded", + thinkingBlocksExpansion: "collapsed", showMessageTimeline: true, showTimelineTools: true, holdLongAssistantReplies: true, promptSubmitOnEnter: false, showPromptVoiceInput: true, diffViewMode: "split", + toolCallExpansionDefaults: defaultToolCallExpansionDefaults, toolOutputExpansion: "expanded", diagnosticsExpansion: "expanded", toolInputsVisibility: "collapsed", @@ -160,6 +176,45 @@ const defaultUiSettings: UiSettings = { notifyOnIdle: true, } +function normalizeExpansionPreference(value: unknown, fallback: ExpansionPreference): ExpansionPreference { + return value === "expanded" || value === "collapsed" ? value : fallback +} + +function normalizeToolCallExpansionPreset(value: unknown): ToolCallExpansionPresetSelection { + if (value === "minimal" || value === "balanced" || value === "detailed" || value === "everything" || value === "custom") { + return value + } + return defaultToolCallExpansionDefaults.preset +} + +function normalizeToolCallExpansionTools(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) return {} + const next: Record = {} + for (const [tool, mode] of Object.entries(value as Record)) { + if (!tool) continue + if (mode === "expanded" || mode === "collapsed") { + next[tool] = mode + } + } + return next +} + +function normalizeToolCallExpansionDefaults(input: unknown, legacySettings: Partial): ToolCallExpansionDefaults { + const source = input && typeof input === "object" && !Array.isArray(input) + ? (input as Partial) + : undefined + const legacyThinking = normalizeExpansionPreference( + legacySettings.thinkingBlocksExpansion, + defaultToolCallExpansionDefaults.thinking, + ) + + return { + preset: normalizeToolCallExpansionPreset(source?.preset), + thinking: normalizeExpansionPreference(source?.thinking, legacyThinking), + tools: normalizeToolCallExpansionTools(source?.tools), + } +} + const defaultSpeechSettings: SpeechSettings = { provider: "openai-compatible", hasApiKey: false, @@ -184,6 +239,7 @@ function normalizeUiSettings(input?: Partial | null): UiSettings { showPromptVoiceInput: sanitized.showPromptVoiceInput ?? defaultUiSettings.showPromptVoiceInput, locale: sanitized.locale ?? defaultUiSettings.locale, diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode, + toolCallExpansionDefaults: normalizeToolCallExpansionDefaults(sanitized.toolCallExpansionDefaults, sanitized), toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion, diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion, toolInputsVisibility: @@ -724,8 +780,19 @@ function setDiffViewMode(mode: DiffViewMode): void { } function setToolOutputExpansion(mode: ExpansionPreference): void { - if (preferences().toolOutputExpansion === mode) return - updateUiSettings({ toolOutputExpansion: mode }) + const current = preferences() + if (current.toolOutputExpansion === mode && current.toolCallExpansionDefaults.tools.other === mode) return + updateUiSettings({ + toolOutputExpansion: mode, + toolCallExpansionDefaults: { + ...current.toolCallExpansionDefaults, + preset: "custom", + tools: { + ...current.toolCallExpansionDefaults.tools, + other: mode, + }, + }, + }) } function setDiagnosticsExpansion(mode: ExpansionPreference): void { @@ -739,8 +806,16 @@ function setToolInputsVisibility(mode: ToolInputsVisibilityPreference): void { } function setThinkingBlocksExpansion(mode: ExpansionPreference): void { - if (preferences().thinkingBlocksExpansion === mode) return - updateUiSettings({ thinkingBlocksExpansion: mode }) + const current = preferences() + if (current.thinkingBlocksExpansion === mode && current.toolCallExpansionDefaults.thinking === mode) return + updateUiSettings({ + thinkingBlocksExpansion: mode, + toolCallExpansionDefaults: { + ...current.toolCallExpansionDefaults, + preset: "custom", + thinking: mode, + }, + }) } function toggleShowThinkingBlocks(): void { diff --git a/packages/ui/src/styles/components/settings-screen.css b/packages/ui/src/styles/components/settings-screen.css index 56d2f1515..c65f4ebf7 100644 --- a/packages/ui/src/styles/components/settings-screen.css +++ b/packages/ui/src/styles/components/settings-screen.css @@ -483,6 +483,116 @@ opacity: 1; } +.settings-expansion-presets { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.settings-expansion-preset { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + min-width: 0; + min-height: 5.25rem; + padding: 0.85rem; + border: 1px solid var(--border-base); + border-radius: 0; + background: linear-gradient(180deg, color-mix(in oklab, var(--surface-base) 88%, var(--surface-secondary)), var(--surface-base)); + color: var(--text-primary); + text-align: start; + cursor: pointer; + transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; +} + +.settings-expansion-preset:hover { + border-color: color-mix(in oklab, var(--accent-primary) 28%, var(--border-base)); + background: var(--surface-hover); +} + +.settings-expansion-preset:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} + +.settings-expansion-preset[data-selected="true"] { + border-color: color-mix(in oklab, var(--accent-primary) 48%, var(--border-base)); + background: color-mix(in oklab, var(--accent-primary) 10%, var(--surface-base)); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent-primary) 20%, transparent); + transform: translateY(-1px); +} + +.settings-expansion-preset-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); +} + +.settings-expansion-preset-copy { + font-size: var(--font-size-xs); + line-height: var(--line-height-snug); + color: var(--text-muted); +} + +.settings-expansion-table { + margin-bottom: 1.25rem; + border: 1px solid var(--border-base); + border-radius: 0; + background: var(--surface-base); + overflow: hidden; +} + +.settings-expansion-table-header, +.settings-expansion-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(11rem, 13rem); + align-items: center; + gap: 1rem; +} + +.settings-expansion-table-header { + padding: 0.65rem 0.875rem; + border-bottom: 1px solid var(--border-base); + background: color-mix(in oklab, var(--surface-secondary) 82%, var(--surface-base)); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.settings-expansion-row { + padding: 0.625rem 0.875rem; + border-top: 1px solid color-mix(in oklab, var(--border-base) 72%, transparent); +} + +.settings-expansion-table-header + .settings-expansion-row { + border-top: none; +} + +.settings-expansion-row-label { + min-width: 0; +} + +.settings-expansion-row-label code { + font-family: var(--font-family-mono); + font-size: var(--font-size-sm); + color: var(--text-primary); + background: transparent; + border: none; + padding: 0; +} + +.settings-expansion-row-control { + min-width: 0; +} + +.settings-expansion-select { + width: 100%; +} + .settings-toggle-row { display: flex; align-items: center; @@ -598,6 +708,10 @@ width: auto; flex-shrink: 0; } + + .settings-expansion-presets { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 640px) { @@ -666,4 +780,23 @@ .settings-choice-grid { grid-template-columns: 1fr; } + + .settings-expansion-presets { + grid-template-columns: 1fr; + } + + .settings-expansion-table-header { + display: none; + } + + .settings-expansion-row { + grid-template-columns: 1fr; + gap: 0.55rem; + align-items: stretch; + padding: 0.8rem; + } + + .settings-expansion-row + .settings-expansion-row { + border-top: 1px solid color-mix(in oklab, var(--border-base) 72%, transparent); + } }