Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/ui/src/components/message-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}`
})
Expand Down Expand Up @@ -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}
Expand Down
173 changes: 165 additions & 8 deletions packages/ui/src/components/settings/appearance-settings-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ 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 },
{ value: "light", icon: Sun },
{ value: "dark", icon: Moon },
]

const toolExpansionPresetOptions: ToolCallExpansionPreset[] = ["minimal", "balanced", "detailed", "everything"]

export const AppearanceSettingsSection: Component = () => {
const { t } = useI18n()
const { themeMode, setThemeMode } = useTheme()
Expand Down Expand Up @@ -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<Map<string, unknown>>(new Map())
Expand Down Expand Up @@ -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<SelectOption[]>(() => [
{ value: "collapsed", label: t("commands.common.collapsed") },
{ value: "expanded", label: t("commands.common.expanded") },
])

const toolExpansionRows = createMemo<ExpansionRow[]>(() => [
{ 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<string, ExpansionPreference> = {}
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))
Expand Down Expand Up @@ -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 (
<div class="settings-section-stack">
<div class="settings-card">
Expand Down Expand Up @@ -266,7 +350,80 @@ export const AppearanceSettingsSection: Component = () => {
<h3 class="settings-card-title">{t("settings.appearance.behavior.title")}</h3>
<p class="settings-card-subtitle">{t("settings.appearance.behavior.subtitle")}</p>
</div>
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
<span class="settings-scope-badge">{presetLabel(currentPreset())}</span>
</div>

<div class="settings-expansion-presets" aria-label={t("settings.behavior.expansionPresets.ariaLabel")}>
<For each={toolExpansionPresetOptions}>
{(preset) => (
<button
type="button"
class="settings-expansion-preset"
data-selected={currentPreset() === preset ? "true" : "false"}
onClick={() => applyExpansionPreset(preset)}
>
<span class="settings-expansion-preset-title">{presetLabel(preset)}</span>
<span class="settings-expansion-preset-copy">{t(`settings.behavior.expansionPreset.${preset}.description`)}</span>
</button>
)}
</For>
</div>

<div class="settings-expansion-table" role="table" aria-label={t("settings.behavior.expansionDefaults.title")}>
<div class="settings-expansion-table-header" role="row">
<span role="columnheader">{t("settings.behavior.expansionDefaults.itemColumn")}</span>
<span role="columnheader">{t("settings.behavior.expansionDefaults.stateColumn")}</span>
</div>
<For each={toolExpansionRows()}>
{(row) => {
const selected = createMemo(() => selectedExpansionOption(rowMode(row)))
return (
<div class="settings-expansion-row" role="row">
<div class="settings-expansion-row-label" role="cell">
<code>{row.label}</code>
</div>
<div class="settings-expansion-row-control" role="cell">
<Select<SelectOption>
value={selected()}
onChange={(opt) => {
if (!opt) return
setExpansionRowMode(row, opt.value as ExpansionPreference)
}}
options={expansionOptions()}
optionValue="value"
optionTextValue="label"
itemComponent={(itemProps) => (
<Select.Item item={itemProps.item} class="selector-option">
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel>
</Select.Item>
)}
>
<Select.Trigger class="selector-trigger settings-expansion-select" aria-label={t("settings.behavior.expansionDefaults.rowAriaLabel", { item: row.label })}>
<div class="flex-1 min-w-0">
<Select.Value<SelectOption>>
{(state) => (
<span class="selector-trigger-primary selector-trigger-primary--align-left">
{state.selectedOption()?.label}
</span>
)}
</Select.Value>
</div>
<Select.Icon class="selector-trigger-icon">
<ChevronDown class="w-3 h-3" />
</Select.Icon>
</Select.Trigger>

<Select.Portal>
<Select.Content class="selector-popover">
<Select.Listbox class="selector-listbox" />
</Select.Content>
</Select.Portal>
</Select>
</div>
</div>
)
}}
</For>
</div>

<div class="settings-stack">
Expand Down
30 changes: 17 additions & 13 deletions packages/ui/src/components/tool-call.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<boolean | null>(null)
Expand Down Expand Up @@ -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()
Expand Down
46 changes: 2 additions & 44 deletions packages/ui/src/components/tool-call/renderers/index.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, ToolRenderer>>((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"
Loading
Loading