diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index dacc55df5..2e385eec9 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -1066,6 +1066,18 @@ def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypat ) assert resp_show["result"]["value"] == "show" assert server._sessions["sid"]["show_reasoning"] is True + assert server._load_cfg()["display"]["sections"]["thinking"] == "expanded" + + resp_hide = server.handle_request( + { + "id": "3", + "method": "config.set", + "params": {"session_id": "sid", "key": "reasoning", "value": "hide"}, + } + ) + assert resp_hide["result"]["value"] == "hide" + assert server._sessions["sid"]["show_reasoning"] is False + assert server._load_cfg()["display"]["sections"]["thinking"] == "hidden" def test_config_set_verbose_updates_session_mode_and_agent(tmp_path, monkeypatch): diff --git a/tui_gateway/server.py b/tui_gateway/server.py index fee8e9550..24f6baf71 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3105,12 +3105,34 @@ def _(rid, params: dict) -> dict: arg = str(value or "").strip().lower() if arg in ("show", "on"): - _write_config_key("display.show_reasoning", True) + cfg = _load_cfg() + display = cfg.get("display") if isinstance(cfg.get("display"), dict) else {} + sections = ( + display.get("sections") + if isinstance(display.get("sections"), dict) + else {} + ) + display["show_reasoning"] = True + sections["thinking"] = "expanded" + display["sections"] = sections + cfg["display"] = display + _save_cfg(cfg) if session: session["show_reasoning"] = True return _ok(rid, {"key": key, "value": "show"}) if arg in ("hide", "off"): - _write_config_key("display.show_reasoning", False) + cfg = _load_cfg() + display = cfg.get("display") if isinstance(cfg.get("display"), dict) else {} + sections = ( + display.get("sections") + if isinstance(display.get("sections"), dict) + else {} + ) + display["show_reasoning"] = False + sections["thinking"] = "hidden" + display["sections"] = sections + cfg["display"] = display + _save_cfg(cfg) if session: session["show_reasoning"] = False return _ok(rid, {"key": key, "value": "hide"}) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 3ec340b8a..56128e388 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -76,6 +76,45 @@ describe('createSlashHandler', () => { }) }) + it('applies /reasoning hide to the thinking section immediately', async () => { + patchUiState({ sections: { thinking: 'expanded' }, showReasoning: true, sid: 'sid-abc' }) + const ctx = buildCtx({ + gateway: { + ...buildGateway(), + rpc: vi.fn(() => Promise.resolve({ value: 'hide' })) + } + }) + + expect(createSlashHandler(ctx)('/reasoning hide')).toBe(true) + + await vi.waitFor(() => { + expect(getUiState().showReasoning).toBe(false) + expect(getUiState().sections.thinking).toBe('hidden') + }) + expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { + key: 'reasoning', + session_id: 'sid-abc', + value: 'hide' + }) + }) + + it('applies /reasoning show to the thinking section immediately', async () => { + patchUiState({ sections: { thinking: 'hidden' }, showReasoning: false, sid: 'sid-abc' }) + const ctx = buildCtx({ + gateway: { + ...buildGateway(), + rpc: vi.fn(() => Promise.resolve({ value: 'show' })) + } + }) + + expect(createSlashHandler(ctx)('/reasoning show')).toBe(true) + + await vi.waitFor(() => { + expect(getUiState().showReasoning).toBe(true) + expect(getUiState().sections.thinking).toBe('expanded') + }) + }) + it('opens the skills hub locally for bare /skills', () => { const ctx = buildCtx() diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index ecd1b7866..cfe84f942 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -332,7 +332,29 @@ export const sessionCommands: SlashCommand[] = [ ctx.gateway .rpc('config.set', { key: 'reasoning', session_id: ctx.sid, value: arg }) - .then(ctx.guarded(r => r.value && ctx.transcript.sys(`reasoning: ${r.value}`))) + .then( + ctx.guarded(r => { + if (!r.value) { + return + } + + if (r.value === 'hide') { + patchUiState(state => ({ + ...state, + sections: { ...state.sections, thinking: 'hidden' }, + showReasoning: false + })) + } else if (r.value === 'show') { + patchUiState(state => ({ + ...state, + sections: { ...state.sections, thinking: 'expanded' }, + showReasoning: true + })) + } + + ctx.transcript.sys(`reasoning: ${r.value}`) + }) + ) } }, diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index 70dc96fec..9ec18337b 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -711,6 +711,9 @@ export function useMainApp(gw: GatewayClient) { const anyPanelVisible = SECTION_NAMES.some( s => sectionMode(s, ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden' ) + const thinkingPanelVisible = sectionMode('thinking', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden' + const toolsPanelVisible = sectionMode('tools', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden' + const activityPanelVisible = sectionMode('activity', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden' const showProgressArea = useTurnSelector(state => anyPanelVisible @@ -718,12 +721,25 @@ export function useMainApp(gw: GatewayClient) { ui.busy || state.outcome || state.streamPendingTools.length || - state.streamSegments.length || + state.streamSegments.some(segment => { + const hasThinking = Boolean(segment.thinking?.trim()) + const hasTrailTools = Boolean(segment.tools?.length) + + if (segment.kind === 'trail' && !segment.text) { + return (thinkingPanelVisible && hasThinking) || ((toolsPanelVisible || activityPanelVisible) && hasTrailTools) + } + + return ( + Boolean(segment.text?.trim()) || + (thinkingPanelVisible && hasThinking) || + ((toolsPanelVisible || activityPanelVisible) && hasTrailTools) + ) + }) || state.subagents.length || state.tools.length || state.todos.length || state.turnTrail.length || - hasReasoning || + (thinkingPanelVisible && hasReasoning) || state.activity.length ) : state.activity.some(item => item.tone !== 'info')