diff --git a/ui-tui/packages/hermes-ink/src/ink/ink.tsx b/ui-tui/packages/hermes-ink/src/ink/ink.tsx index d05b743a09..ff6570f8cf 100644 --- a/ui-tui/packages/hermes-ink/src/ink/ink.tsx +++ b/ui-tui/packages/hermes-ink/src/ink/ink.tsx @@ -62,10 +62,10 @@ import { getSelectedText, hasSelection, moveFocus, + selectionBounds, type SelectionState, selectLineAt, selectWordAt, - selectionBounds, shiftAnchor, shiftSelection, shiftSelectionForFollow, diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 32c92c00ab..a51c89c5b8 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -119,6 +119,7 @@ describe('createSlashHandler', () => { expect(getUiState().detailsMode).toBe('collapsed') expect(createSlashHandler(ctx)('/details toggle')).toBe(true) expect(getUiState().detailsMode).toBe('expanded') + expect(getUiState().detailsModeCommandOverride).toBe(true) expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'details_mode', value: 'expanded' diff --git a/ui-tui/src/__tests__/details.test.ts b/ui-tui/src/__tests__/details.test.ts index 0f567b2f72..04a1fca90e 100644 --- a/ui-tui/src/__tests__/details.test.ts +++ b/ui-tui/src/__tests__/details.test.ts @@ -78,19 +78,25 @@ describe('sectionMode', () => { expect(sectionMode('subagents', 'hidden', {})).toBe('hidden') }) - it('streams thinking + tools expanded by default regardless of global mode', () => { + it('streams thinking + tools expanded by default for persisted config values', () => { expect(sectionMode('thinking', 'collapsed', {})).toBe('expanded') expect(sectionMode('thinking', 'hidden', undefined)).toBe('expanded') expect(sectionMode('tools', 'collapsed', {})).toBe('expanded') expect(sectionMode('tools', 'hidden', undefined)).toBe('expanded') }) - it('hides the activity panel by default regardless of global mode', () => { + it('hides the activity panel by default for persisted config values', () => { expect(sectionMode('activity', 'collapsed', {})).toBe('hidden') expect(sectionMode('activity', 'expanded', undefined)).toBe('hidden') expect(sectionMode('activity', 'hidden', {})).toBe('hidden') }) + it('applies in-session /details mode globally over built-in defaults', () => { + expect(sectionMode('thinking', 'collapsed', {}, true)).toBe('collapsed') + expect(sectionMode('tools', 'hidden', {}, true)).toBe('hidden') + expect(sectionMode('activity', 'expanded', undefined, true)).toBe('expanded') + }) + it('honours per-section overrides over both the section default and global mode', () => { expect(sectionMode('thinking', 'collapsed', { thinking: 'collapsed' })).toBe('collapsed') expect(sectionMode('tools', 'collapsed', { tools: 'hidden' })).toBe('hidden') diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index 032eee87ab..34919aca02 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -90,6 +90,7 @@ export interface UiState { busy: boolean compact: boolean detailsMode: DetailsMode + detailsModeCommandOverride: boolean info: null | SessionInfo inlineDiffs: boolean mouseTracking: boolean diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 6d927fedcc..70804a1f5b 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -184,7 +184,7 @@ export const coreCommands: SlashCommand[] = [ } const mode = parseDetailsMode(r?.value) ?? ui.detailsMode - patchUiState({ detailsMode: mode }) + patchUiState({ detailsMode: mode, detailsModeCommandOverride: false }) const overrides = SECTION_NAMES.filter(s => ui.sections[s]) .map(s => `${s}=${ui.sections[s]}`) @@ -224,7 +224,7 @@ export const coreCommands: SlashCommand[] = [ return transcript.sys(DETAILS_USAGE) } - patchUiState({ detailsMode: next }) + patchUiState({ detailsMode: next, detailsModeCommandOverride: true }) gateway.rpc('config.set', { key: 'details_mode', value: next }).catch(() => {}) transcript.sys(`details: ${next}`) } diff --git a/ui-tui/src/app/uiStore.ts b/ui-tui/src/app/uiStore.ts index fc17a6948f..1b3a841e18 100644 --- a/ui-tui/src/app/uiStore.ts +++ b/ui-tui/src/app/uiStore.ts @@ -11,6 +11,7 @@ const buildUiState = (): UiState => ({ busy: false, compact: false, detailsMode: 'collapsed', + detailsModeCommandOverride: false, info: null, inlineDiffs: true, mouseTracking: MOUSE_TRACKING, diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts index 3ceb8c635a..26d02d6204 100644 --- a/ui-tui/src/app/useConfigSync.ts +++ b/ui-tui/src/app/useConfigSync.ts @@ -45,6 +45,7 @@ export const applyDisplay = (cfg: ConfigFullResponse | null, setBell: (v: boolea patchUiState({ compact: !!d.tui_compact, detailsMode: resolveDetailsMode(d), + detailsModeCommandOverride: false, inlineDiffs: d.inline_diffs !== false, mouseTracking: d.tui_mouse !== false, sections: resolveSections(d.sections), diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index d46744a032..6e07f8f8c1 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -26,6 +26,7 @@ import { createGatewayEventHandler } from './createGatewayEventHandler.js' import { createSlashHandler } from './createSlashHandler.js' import { type GatewayRpc, type TranscriptRow } from './interfaces.js' import { $overlayState, patchOverlayState } from './overlayStore.js' +import { scrollWithSelectionBy } from './scroll.js' import { turnController } from './turnController.js' import { $turnState, patchTurnState } from './turnStore.js' import { $uiState, getUiState, patchUiState } from './uiStore.js' @@ -33,7 +34,6 @@ import { useComposerState } from './useComposerState.js' import { useConfigSync } from './useConfigSync.js' import { useInputHandlers } from './useInputHandlers.js' import { useLongRunToolCharms } from './useLongRunToolCharms.js' -import { scrollWithSelectionBy } from './scroll.js' import { useSessionLifecycle } from './useSessionLifecycle.js' import { useSubmission } from './useSubmission.js' @@ -593,7 +593,9 @@ export function useMainApp(gw: GatewayClient) { // resolved to hidden, the only thing ToolTrail will surface is the // floating-alert backstop (errors/warnings). Mirror that so we don't // render an empty wrapper Box above the streaming area in quiet mode. - const anyPanelVisible = SECTION_NAMES.some(s => sectionMode(s, ui.detailsMode, ui.sections) !== 'hidden') + const anyPanelVisible = SECTION_NAMES.some( + s => sectionMode(s, ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden' + ) const showProgressArea = anyPanelVisible ? Boolean( diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 170d0649ac..b302fed66f 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -25,6 +25,7 @@ const StreamingAssistant = memo(function StreamingAssistant({ cols, compact, detailsMode, + detailsModeCommandOverride, progress, sections, t @@ -40,6 +41,7 @@ const StreamingAssistant = memo(function StreamingAssistant({ cols={cols} compact={compact} detailsMode={detailsMode} + detailsModeCommandOverride={detailsModeCommandOverride} key={`seg:${i}`} msg={msg} sections={sections} @@ -52,6 +54,7 @@ const StreamingAssistant = memo(function StreamingAssistant({