+
+
-
{label}
+
+ {isStreaming ? (
+
+ {t('chat.reasoningTrace.reasoning')}
+
+
+ ) : isExpanded ? (
+
+ {t(variant === 'justification' ? 'chat.reasoningTrace.justification' : 'chat.reasoningTrace.thinking')}
+
+ ) : (
+
+ {t(variant === 'justification' ? 'chat.reasoningTrace.justification' : 'chat.reasoningTrace.thinking')}
+
+ )}
- {(summary || (showDuration && typeof timeStart === 'number')) ? (
-
- {summary ? {summary} : null}
- {showDuration && typeof timeStart === 'number' ? (
-
-
-
-
-
- ) : null}
-
- ) : null}
+
+ {!isStreaming && !isExpanded && summary ? (
+
+ {summary}
+
+ ) : (
+
+ )}
+
+ {/* Expanded content — left border matching ToolPart */}
{isExpanded && (
+
void;
messageId: string;
- alwaysShowActions?: boolean;
};
const ReasoningPart = React.memo(({
part,
onContentChange,
messageId,
- alwaysShowActions = false,
}: ReasoningPartProps) => {
const chatRenderMode = useUIStore((state) => state.chatRenderMode);
const partWithText = part as PartWithText;
@@ -233,9 +323,84 @@ const ReasoningPart = React.memo(({
onContentChange={onContentChange}
blockId={part.id || `${messageId}-reasoning`}
time={time}
- showDuration={chatRenderMode !== 'sorted'}
isStreaming={isStreaming}
- alwaysShowActions={alwaysShowActions}
+ />
+ );
+});
+
+type MergedReasoningPartProps = {
+ parts: Part[];
+ onContentChange?: (reason?: ContentChangeReason) => void;
+ messageId: string;
+};
+
+/**
+ * Renders ALL reasoning parts for a message as a single collapsible block,
+ * merging their text and spanning their combined time range.
+ * This matches the VSCode Copilot pattern of showing one "Thought" block per turn.
+ */
+export const MergedReasoningPart = React.memo(({
+ parts,
+ onContentChange,
+ messageId,
+}: MergedReasoningPartProps) => {
+ const chatRenderMode = useUIStore((state) => state.chatRenderMode);
+
+ const mergedText = React.useMemo(() => {
+ return parts
+ .map((part) => {
+ const p = part as PartWithText;
+ return cleanReasoningText(p.text || p.content || '');
+ })
+ .filter((t) => t.length > 0)
+ .join('\n\n');
+ }, [parts]);
+
+ const mergedTime = React.useMemo(() => {
+ let earliestStart: number | undefined;
+ let latestEnd: number | undefined;
+
+ for (const part of parts) {
+ const time = (part as PartWithText).time;
+ if (typeof time?.start === 'number' && Number.isFinite(time.start)) {
+ if (earliestStart === undefined || time.start < earliestStart) {
+ earliestStart = time.start;
+ }
+ }
+ if (typeof time?.end === 'number' && Number.isFinite(time.end)) {
+ if (latestEnd === undefined || time.end > latestEnd) {
+ latestEnd = time.end;
+ }
+ }
+ }
+
+ return earliestStart !== undefined ? { start: earliestStart, end: latestEnd } : undefined;
+ }, [parts]);
+
+ const isStreaming = chatRenderMode === 'live' && parts.some(
+ (part) => typeof (part as PartWithText).time?.end !== 'number',
+ );
+
+ const throttledMergedText = useStreamingTextThrottle({
+ text: mergedText,
+ isStreaming,
+ identityKey: `${messageId}:reasoning-merged`,
+ });
+
+ const blockId = parts[0]?.id ?? `${messageId}-reasoning-merged`;
+
+ if (!throttledMergedText.trim()) {
+ return null;
+ }
+
+ return (
+
);
});
diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx
index dd3becfa..4c3fd11e 100644
--- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx
+++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx
@@ -249,6 +249,8 @@ export const OpenChamberVisualSettings: React.FC
const directoryShowHidden = useDirectoryShowHidden();
const showReasoningTraces = useUIStore(state => state.showReasoningTraces);
const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces);
+ const collapsibleThinkingBlocks = useUIStore(state => state.collapsibleThinkingBlocks);
+ const setCollapsibleThinkingBlocks = useUIStore(state => state.setCollapsibleThinkingBlocks);
const mermaidRenderingMode = useUIStore(state => state.mermaidRenderingMode);
const setMermaidRenderingMode = useUIStore(state => state.setMermaidRenderingMode);
@@ -1556,6 +1558,29 @@ export const OpenChamberVisualSettings: React.FC
)}
+ {shouldShow('reasoning') && showReasoningTraces && (
+ setCollapsibleThinkingBlocks(!collapsibleThinkingBlocks)}
+ onKeyDown={(event) => {
+ if (event.key === ' ' || event.key === 'Enter') {
+ event.preventDefault();
+ setCollapsibleThinkingBlocks(!collapsibleThinkingBlocks);
+ }
+ }}
+ >
+
+ {t('settings.openchamber.visual.field.collapsibleThinkingBlocks')}
+
+ )}
+
{shouldShow('stickyUserHeader') && (
& {
preventOverscroll?: boolean;
useScrollShadow?: boolean;
scrollShadowSize?: number;
+ userIntentOnly?: boolean;
/** Forwarded to the inner element (e.g. textarea). */
disabled?: boolean;
};
@@ -36,6 +37,7 @@ export const ScrollableOverlay = React.forwardRef {
const containerRef = React.useRef(null);
@@ -91,6 +93,7 @@ export const ScrollableOverlay = React.forwardRef
);
diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css
index 5481f922..dbbc3884 100644
--- a/packages/ui/src/index.css
+++ b/packages/ui/src/index.css
@@ -856,7 +856,7 @@ html:not(.dark) .chat-scroll {
/* Reasoning markdown renders at meta size, dimmed. */
.markdown-content.markdown-reasoning {
- font-size: var(--text-meta);
+ font-size: var(--text-markdown);
color: var(--surface-muted-foreground);
--markdown-heading1: inherit;
--markdown-heading2: inherit;
diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts
index e58d7c4e..2c8d6fd6 100644
--- a/packages/ui/src/lib/api/types.ts
+++ b/packages/ui/src/lib/api/types.ts
@@ -580,6 +580,7 @@ export interface SettingsPayload {
securityScopedBookmarks?: string[];
pinnedDirectories?: string[];
showReasoningTraces?: boolean;
+ collapsibleThinkingBlocks?: boolean;
showDeletionDialog?: boolean;
nativeNotificationsEnabled?: boolean;
notificationMode?: 'always' | 'hidden-only';
diff --git a/packages/ui/src/lib/appearanceAutoSave.ts b/packages/ui/src/lib/appearanceAutoSave.ts
index 5bc6e25d..a18f5511 100644
--- a/packages/ui/src/lib/appearanceAutoSave.ts
+++ b/packages/ui/src/lib/appearanceAutoSave.ts
@@ -6,6 +6,7 @@ import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
type AppearanceSlice = {
showReasoningTraces: boolean;
+ collapsibleThinkingBlocks: boolean;
showDeletionDialog: boolean;
nativeNotificationsEnabled: boolean;
notificationMode: 'always' | 'hidden-only';
@@ -50,6 +51,7 @@ export const startAppearanceAutoSave = (): void => {
let previous: AppearanceSlice = {
showReasoningTraces: useUIStore.getState().showReasoningTraces,
+ collapsibleThinkingBlocks: useUIStore.getState().collapsibleThinkingBlocks,
showDeletionDialog: useUIStore.getState().showDeletionDialog,
nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled,
notificationMode: useUIStore.getState().notificationMode,
@@ -101,6 +103,7 @@ export const startAppearanceAutoSave = (): void => {
useUIStore.subscribe((state) => {
const current: AppearanceSlice = {
showReasoningTraces: state.showReasoningTraces,
+ collapsibleThinkingBlocks: state.collapsibleThinkingBlocks,
showDeletionDialog: state.showDeletionDialog,
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
notificationMode: state.notificationMode,
@@ -134,6 +137,9 @@ export const startAppearanceAutoSave = (): void => {
if (current.showReasoningTraces !== previous.showReasoningTraces) {
diff.showReasoningTraces = current.showReasoningTraces;
}
+ if (current.collapsibleThinkingBlocks !== previous.collapsibleThinkingBlocks) {
+ diff.collapsibleThinkingBlocks = current.collapsibleThinkingBlocks;
+ }
if (current.showDeletionDialog !== previous.showDeletionDialog) {
diff.showDeletionDialog = current.showDeletionDialog;
}
diff --git a/packages/ui/src/lib/appearancePersistence.ts b/packages/ui/src/lib/appearancePersistence.ts
index e133602b..41952204 100644
--- a/packages/ui/src/lib/appearancePersistence.ts
+++ b/packages/ui/src/lib/appearancePersistence.ts
@@ -2,10 +2,12 @@ import { useUIStore } from '@/stores/useUIStore';
export interface AppearancePreferences {
showReasoningTraces?: boolean;
+ collapsibleThinkingBlocks?: boolean;
}
type RawAppearancePayload = {
showReasoningTraces?: unknown;
+ collapsibleThinkingBlocks?: unknown;
};
const sanitizePreferences = (payload?: RawAppearancePayload | null): AppearancePreferences | null => {
@@ -19,6 +21,10 @@ const sanitizePreferences = (payload?: RawAppearancePayload | null): AppearanceP
result.showReasoningTraces = payload.showReasoningTraces;
}
+ if (typeof payload.collapsibleThinkingBlocks === 'boolean') {
+ result.collapsibleThinkingBlocks = payload.collapsibleThinkingBlocks;
+ }
+
return Object.keys(result).length > 0 ? result : null;
};
@@ -30,6 +36,7 @@ const extractRawAppearance = (data: unknown): RawAppearancePayload | null => {
const candidate = data as Record;
const payload: RawAppearancePayload = {
showReasoningTraces: candidate.showReasoningTraces,
+ collapsibleThinkingBlocks: candidate.collapsibleThinkingBlocks,
};
return payload;
@@ -54,6 +61,10 @@ export const applyAppearancePreferences = (preferences: AppearancePreferences):
if (typeof preferences.showReasoningTraces === 'boolean') {
store.setShowReasoningTraces(preferences.showReasoningTraces);
}
+
+ if (typeof preferences.collapsibleThinkingBlocks === 'boolean') {
+ store.setCollapsibleThinkingBlocks(preferences.collapsibleThinkingBlocks);
+ }
};
export const loadAppearancePreferences = async (): Promise => {
diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts
index c11d6751..d4535354 100644
--- a/packages/ui/src/lib/desktop.ts
+++ b/packages/ui/src/lib/desktop.ts
@@ -58,6 +58,7 @@ export type DesktopSettings = {
securityScopedBookmarks?: string[];
pinnedDirectories?: string[];
showReasoningTraces?: boolean;
+ collapsibleThinkingBlocks?: boolean;
showDeletionDialog?: boolean;
nativeNotificationsEnabled?: boolean;
notificationMode?: 'always' | 'hidden-only';
diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts
index 9e6bc1ce..a9c00025 100644
--- a/packages/ui/src/lib/i18n/messages/en.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/en.settings.ts
@@ -1434,6 +1434,8 @@ export const settingsDict = {
'settings.openchamber.visual.field.diffViewModeAria': 'Diff view mode: {option}',
'settings.openchamber.visual.field.showReasoningTracesAria': 'Show reasoning traces',
'settings.openchamber.visual.field.showReasoningTraces': 'Show Reasoning Traces',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Enable collapsible reasoning blocks',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocks': 'Enable Collapsible Reasoning Blocks',
'settings.openchamber.visual.field.stickyUserHeaderAria': 'Sticky user header',
'settings.openchamber.visual.field.stickyUserHeader': 'Sticky User Header',
'settings.openchamber.visual.field.wideChatLayoutAria': 'Wide chat layout',
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts
index 06659e72..37fcc175 100644
--- a/packages/ui/src/lib/i18n/messages/en.ts
+++ b/packages/ui/src/lib/i18n/messages/en.ts
@@ -1686,6 +1686,12 @@ export const dict = {
'chat.modelControls.modeValue.subagent': 'Subagent',
'chat.modelControls.modeValue.all': 'All',
'chat.modelControls.modeValue.none': '—',
+ 'chat.reasoningTrace.thinking': 'Thinking',
+ 'chat.reasoningTrace.reasoning': 'Reasoning',
+ 'chat.reasoningTrace.justification': 'Justification',
+ 'chat.reasoningTrace.expandAria': 'Expand reasoning trace',
+ 'chat.reasoningTrace.collapseAria': 'Collapse reasoning trace',
+ 'chat.reasoningTrace.thought': 'Thought',
'chat.messageBody.subtask.title': 'Delegated task',
'chat.messageBody.subtask.hidePrompt': 'Hide prompt',
'chat.messageBody.subtask.showPrompt': 'Show prompt',
diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts
index 8371e46b..6e8785d7 100644
--- a/packages/ui/src/lib/i18n/messages/es.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/es.settings.ts
@@ -1434,6 +1434,8 @@ export const settingsDict = {
"settings.openchamber.visual.field.diffViewModeAria": "Modo de vista de comparación: {option}",
"settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de razonamiento",
"settings.openchamber.visual.field.showReasoningTraces": "Mostrar trazas de razonamiento",
+ "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar bloques de razonamiento colapsables",
+ "settings.openchamber.visual.field.collapsibleThinkingBlocks": "Habilitar Bloques de Razonamiento Colapsables",
"settings.openchamber.visual.field.stickyUserHeaderAria": "Encabezado del usuario fijo",
"settings.openchamber.visual.field.stickyUserHeader": "Encabezado de usuario fijo",
"settings.openchamber.visual.field.wideChatLayoutAria": "Diseño de chat ancho",
diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts
index f9d0a9e4..16e2bfe8 100644
--- a/packages/ui/src/lib/i18n/messages/es.ts
+++ b/packages/ui/src/lib/i18n/messages/es.ts
@@ -1652,6 +1652,12 @@ export const dict: Record = {
"chat.modelControls.modeValue.subagent": "Subagente",
"chat.modelControls.modeValue.all": "Todos",
"chat.modelControls.modeValue.none": "—",
+ "chat.reasoningTrace.thinking": "Pensando",
+ "chat.reasoningTrace.reasoning": "Razonando",
+ "chat.reasoningTrace.justification": "Justificación",
+ "chat.reasoningTrace.expandAria": "Expandir traza de razonamiento",
+ "chat.reasoningTrace.collapseAria": "Contraer traza de razonamiento",
+ "chat.reasoningTrace.thought": "Pensó",
"chat.messageBody.subtask.title": "Tarea delegada",
"chat.messageBody.subtask.hidePrompt": "Ocultar prompt",
"chat.messageBody.subtask.showPrompt": "Mostrar prompt",
diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts
index 48c4a6db..23ac2c79 100644
--- a/packages/ui/src/lib/i18n/messages/ko.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts
@@ -1434,6 +1434,8 @@ export const settingsDict = {
'settings.openchamber.visual.field.diffViewModeAria': 'Diff 보기 모드: {option}',
'settings.openchamber.visual.field.showReasoningTracesAria': 'Reasoning trace 표시',
'settings.openchamber.visual.field.showReasoningTraces': 'Reasoning Trace 표시',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '접을 수 있는 추론 블록 활성화',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocks': '접을 수 있는 추론 블록 활성화',
'settings.openchamber.visual.field.stickyUserHeaderAria': '고정 사용자 헤더',
'settings.openchamber.visual.field.stickyUserHeader': '고정 사용자 헤더',
'settings.openchamber.visual.field.wideChatLayoutAria': '넓은 채팅 레이아웃',
diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts
index 3598cf33..83c4c99f 100644
--- a/packages/ui/src/lib/i18n/messages/ko.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.ts
@@ -1686,6 +1686,12 @@ export const dict: Record = {
'chat.modelControls.modeValue.subagent': '서브 에이전트',
'chat.modelControls.modeValue.all': '전체',
'chat.modelControls.modeValue.none': '—',
+ 'chat.reasoningTrace.thinking': '추론',
+ 'chat.reasoningTrace.reasoning': '추론 중',
+ 'chat.reasoningTrace.justification': '근거',
+ 'chat.reasoningTrace.expandAria': '추론 트레이스 펼치기',
+ 'chat.reasoningTrace.collapseAria': '추론 트레이스 접기',
+ 'chat.reasoningTrace.thought': '추론 완료',
'chat.messageBody.subtask.title': '위임된 작업',
'chat.messageBody.subtask.hidePrompt': '숨기기 프롬프트',
'chat.messageBody.subtask.showPrompt': '표시 프롬프트',
diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts
index 9c93ab1a..f0ee66b2 100644
--- a/packages/ui/src/lib/i18n/messages/pl.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts
@@ -840,6 +840,8 @@ export const settingsDict = {
'settings.openchamber.visual.field.showMobileStatusBarAria': 'Pokaż mobilny pasek statusu',
'settings.openchamber.visual.field.showReasoningTraces': 'Pokaż ślady rozumowania',
'settings.openchamber.visual.field.showReasoningTracesAria': 'Pokaż ślady rozumowania',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocks': 'Włącz zwijalne bloki rozumowania',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Włącz zwijalne bloki rozumowania',
'settings.openchamber.visual.field.showSplitAssistantMessageActions': 'Akcje asystenta w treści',
'settings.openchamber.visual.field.showSplitAssistantMessageActionsAria': 'Akcje asystenta w treści',
'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip': 'Pokaż Kopiuj odpowiedź, Zapisz jako obraz i Odczytaj na głos w blokach tekstu asystenta, które pojawiają się przed późniejszymi wywołaniami narzędzi w tej samej odpowiedzi.',
diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts
index e9e9ef82..4e6d1f7d 100644
--- a/packages/ui/src/lib/i18n/messages/pl.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.ts
@@ -889,6 +889,12 @@ export const dict: Record = {
'chat.messageBody.subtask.openSession': 'Otwórz sesję podzadania',
'chat.messageBody.subtask.showPrompt': 'Pokaż prompt',
'chat.messageBody.subtask.title': 'Zadanie delegowane',
+ 'chat.reasoningTrace.thinking': 'Myślenie',
+ 'chat.reasoningTrace.reasoning': 'Rozumowanie',
+ 'chat.reasoningTrace.justification': 'Uzasadnienie',
+ 'chat.reasoningTrace.expandAria': 'Rozwiń ślad rozumowania',
+ 'chat.reasoningTrace.collapseAria': 'Zwiń ślad rozumowania',
+ 'chat.reasoningTrace.thought': 'Przemyślał',
'chat.mobileStatus.editProjects.empty': 'Brak projektów do edycji',
'chat.mobileStatus.editProjects.footer': 'Przeciągnij elementy, aby zmienić kolejność, lub użyj strzałek do przesuwania. Dotknij „edytuj”, aby zmienić szczegóły.',
'chat.mobileStatus.editProjects.title': 'Edytuj projekty',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
index 25c08aeb..15cac3be 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
@@ -1434,6 +1434,8 @@ export const settingsDict = {
"settings.openchamber.visual.field.diffViewModeAria": "Modo de visualização de comparação: {option}",
"settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de raciocínio",
"settings.openchamber.visual.field.showReasoningTraces": "Mostrar rastros de raciocínio",
+ "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar blocos de raciocínio recolhíveis",
+ "settings.openchamber.visual.field.collapsibleThinkingBlocks": "Habilitar Blocos de Raciocínio Recolhíveis",
"settings.openchamber.visual.field.stickyUserHeaderAria": "Cabeçalho do usuário fixo",
"settings.openchamber.visual.field.stickyUserHeader": "Cabeçalho do usuário fixo",
"settings.openchamber.visual.field.wideChatLayoutAria": "Layout de chat amplo",
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts
index 62aa9bab..d271fbd4 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts
@@ -1652,6 +1652,12 @@ export const dict: Record = {
"chat.modelControls.modeValue.subagent": "Subagente",
"chat.modelControls.modeValue.all": "Todos",
"chat.modelControls.modeValue.none": "—",
+ "chat.reasoningTrace.thinking": "Pensando",
+ "chat.reasoningTrace.reasoning": "Raciocinando",
+ "chat.reasoningTrace.justification": "Justificativa",
+ "chat.reasoningTrace.expandAria": "Expandir rastro de raciocínio",
+ "chat.reasoningTrace.collapseAria": "Recolher rastro de raciocínio",
+ "chat.reasoningTrace.thought": "Pensou",
"chat.messageBody.subtask.title": "Tarefa delegada",
"chat.messageBody.subtask.hidePrompt": "Ocultar prompt",
"chat.messageBody.subtask.showPrompt": "Mostrar prompt",
diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts
index 2d806979..8c395087 100644
--- a/packages/ui/src/lib/i18n/messages/uk.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts
@@ -1434,6 +1434,8 @@ export const settingsDict = {
"settings.openchamber.visual.field.diffViewModeAria": "Режим перегляду diff: {option}",
"settings.openchamber.visual.field.showReasoningTracesAria": "Показати сліди міркувань",
"settings.openchamber.visual.field.showReasoningTraces": "Показати сліди міркувань",
+ "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Увімкнути згортальні блоки міркувань",
+ "settings.openchamber.visual.field.collapsibleThinkingBlocks": "Увімкнути згортальні блоки міркувань",
"settings.openchamber.visual.field.stickyUserHeaderAria": "Закріплений заголовок користувача",
"settings.openchamber.visual.field.stickyUserHeader": "Закріплений заголовок користувача",
"settings.openchamber.visual.field.wideChatLayoutAria": "Широкий макет чату",
diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts
index c224ca70..096a7ae4 100644
--- a/packages/ui/src/lib/i18n/messages/uk.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.ts
@@ -1652,6 +1652,12 @@ export const dict: Record = {
"chat.modelControls.modeValue.subagent": "Субагент",
"chat.modelControls.modeValue.all": "всі",
"chat.modelControls.modeValue.none": "—",
+ "chat.reasoningTrace.thinking": "Міркування",
+ "chat.reasoningTrace.reasoning": "Міркую",
+ "chat.reasoningTrace.justification": "Обґрунтування",
+ "chat.reasoningTrace.expandAria": "Розгорнути слід міркування",
+ "chat.reasoningTrace.collapseAria": "Згорнути слід міркування",
+ "chat.reasoningTrace.thought": "Обдумав",
"chat.messageBody.subtask.title": "Делеговане завдання",
"chat.messageBody.subtask.hidePrompt": "Приховати промпт",
"chat.messageBody.subtask.showPrompt": "Показати промпт",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
index c29c8ae8..2d5c0b7b 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
@@ -1434,6 +1434,8 @@ export const settingsDict = {
'settings.openchamber.visual.field.diffViewModeAria': 'Diff 视图模式:{option}',
'settings.openchamber.visual.field.showReasoningTracesAria': '显示推理轨迹',
'settings.openchamber.visual.field.showReasoningTraces': '显示推理轨迹',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '启用可折叠推理块',
+ 'settings.openchamber.visual.field.collapsibleThinkingBlocks': '启用可折叠推理块',
'settings.openchamber.visual.field.stickyUserHeaderAria': '固定用户消息头',
'settings.openchamber.visual.field.stickyUserHeader': '固定用户消息头',
'settings.openchamber.visual.field.wideChatLayoutAria': '宽聊天布局',
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts
index b3924e3d..2f470d48 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts
@@ -1652,6 +1652,12 @@ export const dict: Record = {
'chat.modelControls.modeValue.subagent': '子智能体',
'chat.modelControls.modeValue.all': '全部',
'chat.modelControls.modeValue.none': '—',
+ 'chat.reasoningTrace.thinking': '思考',
+ 'chat.reasoningTrace.reasoning': '推理',
+ 'chat.reasoningTrace.justification': '理由',
+ 'chat.reasoningTrace.expandAria': '展开推理轨迹',
+ 'chat.reasoningTrace.collapseAria': '折叠推理轨迹',
+ 'chat.reasoningTrace.thought': '已思考',
'chat.messageBody.subtask.title': '委派任务',
'chat.messageBody.subtask.hidePrompt': '隐藏提示',
'chat.messageBody.subtask.showPrompt': '显示提示',
diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts
index 509f0e3e..cbc46e30 100644
--- a/packages/ui/src/lib/persistence.ts
+++ b/packages/ui/src/lib/persistence.ts
@@ -345,6 +345,9 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) {
store.setShowReasoningTraces(settings.showReasoningTraces);
}
+ if (typeof settings.collapsibleThinkingBlocks === 'boolean' && settings.collapsibleThinkingBlocks !== store.collapsibleThinkingBlocks) {
+ store.setCollapsibleThinkingBlocks(settings.collapsibleThinkingBlocks);
+ }
if (typeof settings.autoDeleteEnabled === 'boolean' && settings.autoDeleteEnabled !== store.autoDeleteEnabled) {
store.setAutoDeleteEnabled(settings.autoDeleteEnabled);
}
@@ -639,6 +642,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces;
}
+ if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
+ result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
+ }
if (typeof candidate.autoDeleteEnabled === 'boolean') {
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
}
diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts
index 17d137eb..552b37b8 100644
--- a/packages/ui/src/stores/useUIStore.ts
+++ b/packages/ui/src/stores/useUIStore.ts
@@ -524,6 +524,8 @@ interface UIStore {
eventStreamStatus: EventStreamStatus;
eventStreamHint: string | null;
showReasoningTraces: boolean;
+ collapsibleThinkingBlocks: boolean;
+ groupReasoningBlocks: boolean;
chatRenderMode: ChatRenderMode;
activityRenderMode: ActivityRenderMode;
showDeletionDialog: boolean;
@@ -653,6 +655,7 @@ interface UIStore {
setSettingsRemoteInstancesSelectedId: (instanceId: string | null) => void;
setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void;
setShowReasoningTraces: (value: boolean) => void;
+ setCollapsibleThinkingBlocks: (value: boolean) => void;
setChatRenderMode: (value: ChatRenderMode) => void;
setActivityRenderMode: (value: ActivityRenderMode) => void;
setShowDeletionDialog: (value: boolean) => void;
@@ -783,6 +786,8 @@ export const useUIStore = create()(
eventStreamStatus: 'idle',
eventStreamHint: null,
showReasoningTraces: true,
+ collapsibleThinkingBlocks: true,
+ groupReasoningBlocks: true,
chatRenderMode: 'live',
activityRenderMode: 'summary',
showDeletionDialog: true,
@@ -1420,6 +1425,10 @@ export const useUIStore = create()(
set({ showReasoningTraces: value });
},
+ setCollapsibleThinkingBlocks: (value) => {
+ set({ collapsibleThinkingBlocks: value });
+ },
+
setChatRenderMode: (value) => {
set({ chatRenderMode: value });
},
@@ -2047,6 +2056,7 @@ export const useUIStore = create()(
isSessionCreateDialogOpen: state.isSessionCreateDialogOpen,
// Note: isSettingsDialogOpen intentionally NOT persisted
showReasoningTraces: state.showReasoningTraces,
+ collapsibleThinkingBlocks: state.collapsibleThinkingBlocks,
chatRenderMode: state.chatRenderMode,
activityRenderMode: state.activityRenderMode,
showDeletionDialog: state.showDeletionDialog,
diff --git a/packages/web/server/lib/opencode/settings-helpers.js b/packages/web/server/lib/opencode/settings-helpers.js
index 3bd6c31e..73e9ce32 100644
--- a/packages/web/server/lib/opencode/settings-helpers.js
+++ b/packages/web/server/lib/opencode/settings-helpers.js
@@ -168,6 +168,9 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces;
}
+ if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
+ result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
+ }
if (typeof candidate.showTextJustificationActivity === 'boolean') {
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
}
@@ -728,7 +731,13 @@ export const createSettingsHelpers = (dependencies) => {
? settings.showReasoningTraces
: typeof sanitized.showReasoningTraces === 'boolean'
? sanitized.showReasoningTraces
- : false
+ : false,
+ collapsibleThinkingBlocks:
+ typeof settings.collapsibleThinkingBlocks === 'boolean'
+ ? settings.collapsibleThinkingBlocks
+ : typeof sanitized.collapsibleThinkingBlocks === 'boolean'
+ ? sanitized.collapsibleThinkingBlocks
+ : true,
};
};
diff --git a/packages/web/server/lib/opencode/settings-helpers.test.js b/packages/web/server/lib/opencode/settings-helpers.test.js
index e71866c4..64e859ec 100644
--- a/packages/web/server/lib/opencode/settings-helpers.test.js
+++ b/packages/web/server/lib/opencode/settings-helpers.test.js
@@ -71,4 +71,39 @@ describe('settings helpers', () => {
expect(helpers.sanitizeSettingsUpdate({ mobileKeyboardMode: 'fixed-layout' })).toEqual({});
});
+
+ it('accepts collapsibleThinkingBlocks as a persisted shared setting', () => {
+ const helpers = createTestHelpers();
+
+ expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: true })).toEqual({
+ collapsibleThinkingBlocks: true,
+ });
+ expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: false })).toEqual({
+ collapsibleThinkingBlocks: false,
+ });
+ });
+
+ it('rejects non-boolean collapsibleThinkingBlocks values', () => {
+ const helpers = createTestHelpers();
+
+ expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: 'true' })).toEqual({});
+ expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: 1 })).toEqual({});
+ });
+
+ it('includes collapsibleThinkingBlocks in formatSettingsResponse', () => {
+ const helpers = createTestHelpers();
+
+ const response = helpers.formatSettingsResponse({ collapsibleThinkingBlocks: false });
+ expect(response.collapsibleThinkingBlocks).toBe(false);
+
+ const responseTrue = helpers.formatSettingsResponse({ collapsibleThinkingBlocks: true });
+ expect(responseTrue.collapsibleThinkingBlocks).toBe(true);
+ });
+
+ it('defaults collapsibleThinkingBlocks to true in formatSettingsResponse when absent', () => {
+ const helpers = createTestHelpers();
+
+ const response = helpers.formatSettingsResponse({});
+ expect(response.collapsibleThinkingBlocks).toBe(true);
+ });
});