fix(tui): apply details mode live
This commit is contained in:
parent
6814646b36
commit
a8fcd1c742
12 changed files with 56 additions and 19 deletions
|
|
@ -62,10 +62,10 @@ import {
|
|||
getSelectedText,
|
||||
hasSelection,
|
||||
moveFocus,
|
||||
selectionBounds,
|
||||
type SelectionState,
|
||||
selectLineAt,
|
||||
selectWordAt,
|
||||
selectionBounds,
|
||||
shiftAnchor,
|
||||
shiftSelection,
|
||||
shiftSelectionForFollow,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export interface UiState {
|
|||
busy: boolean
|
||||
compact: boolean
|
||||
detailsMode: DetailsMode
|
||||
detailsModeCommandOverride: boolean
|
||||
info: null | SessionInfo
|
||||
inlineDiffs: boolean
|
||||
mouseTracking: boolean
|
||||
|
|
|
|||
|
|
@ -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<ConfigSetResponse>('config.set', { key: 'details_mode', value: next }).catch(() => {})
|
||||
transcript.sys(`details: ${next}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const buildUiState = (): UiState => ({
|
|||
busy: false,
|
||||
compact: false,
|
||||
detailsMode: 'collapsed',
|
||||
detailsModeCommandOverride: false,
|
||||
info: null,
|
||||
inlineDiffs: true,
|
||||
mouseTracking: MOUSE_TRACKING,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<ToolTrail
|
||||
activity={progress.activity}
|
||||
busy={busy}
|
||||
commandOverride={detailsModeCommandOverride}
|
||||
detailsMode={detailsMode}
|
||||
outcome={progress.outcome}
|
||||
reasoning={progress.reasoning}
|
||||
|
|
@ -73,6 +76,7 @@ const StreamingAssistant = memo(function StreamingAssistant({
|
|||
cols={cols}
|
||||
compact={compact}
|
||||
detailsMode={detailsMode}
|
||||
detailsModeCommandOverride={detailsModeCommandOverride}
|
||||
isStreaming
|
||||
msg={{
|
||||
role: 'assistant',
|
||||
|
|
@ -89,6 +93,7 @@ const StreamingAssistant = memo(function StreamingAssistant({
|
|||
cols={cols}
|
||||
compact={compact}
|
||||
detailsMode={detailsMode}
|
||||
detailsModeCommandOverride={detailsModeCommandOverride}
|
||||
msg={{ kind: 'trail', role: 'system', text: '', tools: progress.streamPendingTools }}
|
||||
sections={sections}
|
||||
t={t}
|
||||
|
|
@ -127,6 +132,7 @@ const TranscriptPane = memo(function TranscriptPane({
|
|||
cols={composer.cols}
|
||||
compact={ui.compact}
|
||||
detailsMode={ui.detailsMode}
|
||||
detailsModeCommandOverride={ui.detailsModeCommandOverride}
|
||||
msg={row.msg}
|
||||
sections={ui.sections}
|
||||
t={ui.theme}
|
||||
|
|
@ -142,6 +148,7 @@ const TranscriptPane = memo(function TranscriptPane({
|
|||
cols={composer.cols}
|
||||
compact={ui.compact}
|
||||
detailsMode={ui.detailsMode}
|
||||
detailsModeCommandOverride={ui.detailsModeCommandOverride}
|
||||
progress={progress}
|
||||
sections={ui.sections}
|
||||
t={ui.theme}
|
||||
|
|
@ -353,6 +360,7 @@ interface StreamingAssistantProps {
|
|||
cols: number
|
||||
compact?: boolean
|
||||
detailsMode: DetailsMode
|
||||
detailsModeCommandOverride: boolean
|
||||
progress: AppLayoutProgressProps
|
||||
sections?: SectionVisibility
|
||||
t: Theme
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const MessageLine = memo(function MessageLine({
|
|||
cols,
|
||||
compact,
|
||||
detailsMode = 'collapsed',
|
||||
detailsModeCommandOverride = false,
|
||||
isStreaming = false,
|
||||
msg,
|
||||
sections,
|
||||
|
|
@ -28,15 +29,16 @@ export const MessageLine = memo(function MessageLine({
|
|||
// feeds Thinking + Tool calls. Gating on every section would let
|
||||
// `thinking` (expanded by default) keep an empty wrapper alive when only
|
||||
// `tools` is hidden — exactly the empty-Box bug Copilot caught.
|
||||
const thinkingMode = sectionMode('thinking', detailsMode, sections)
|
||||
const toolsMode = sectionMode('tools', detailsMode, sections)
|
||||
const activityMode = sectionMode('activity', detailsMode, sections)
|
||||
const thinkingMode = sectionMode('thinking', detailsMode, sections, detailsModeCommandOverride)
|
||||
const toolsMode = sectionMode('tools', detailsMode, sections, detailsModeCommandOverride)
|
||||
const activityMode = sectionMode('activity', detailsMode, sections, detailsModeCommandOverride)
|
||||
const thinking = msg.thinking?.trim() ?? ''
|
||||
|
||||
if (msg.kind === 'trail' && (msg.tools?.length || thinking)) {
|
||||
return thinkingMode !== 'hidden' || toolsMode !== 'hidden' || activityMode !== 'hidden' ? (
|
||||
<Box flexDirection="column">
|
||||
<ToolTrail
|
||||
commandOverride={detailsModeCommandOverride}
|
||||
detailsMode={detailsMode}
|
||||
reasoning={thinking}
|
||||
reasoningTokens={msg.thinkingTokens}
|
||||
|
|
@ -118,6 +120,7 @@ export const MessageLine = memo(function MessageLine({
|
|||
{showDetails && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<ToolTrail
|
||||
commandOverride={detailsModeCommandOverride}
|
||||
detailsMode={detailsMode}
|
||||
reasoning={thinking}
|
||||
reasoningTokens={msg.thinkingTokens}
|
||||
|
|
@ -146,6 +149,7 @@ interface MessageLineProps {
|
|||
cols: number
|
||||
compact?: boolean
|
||||
detailsMode?: DetailsMode
|
||||
detailsModeCommandOverride?: boolean
|
||||
isStreaming?: boolean
|
||||
msg: Msg
|
||||
sections?: SectionVisibility
|
||||
|
|
|
|||
|
|
@ -681,6 +681,7 @@ interface Group {
|
|||
|
||||
export const ToolTrail = memo(function ToolTrail({
|
||||
busy = false,
|
||||
commandOverride = false,
|
||||
detailsMode = 'collapsed',
|
||||
outcome = '',
|
||||
reasoningActive = false,
|
||||
|
|
@ -696,6 +697,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
activity = []
|
||||
}: {
|
||||
busy?: boolean
|
||||
commandOverride?: boolean
|
||||
detailsMode?: DetailsMode
|
||||
outcome?: string
|
||||
reasoningActive?: boolean
|
||||
|
|
@ -712,12 +714,12 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
}) {
|
||||
const visible = useMemo(
|
||||
() => ({
|
||||
thinking: sectionMode('thinking', detailsMode, sections),
|
||||
tools: sectionMode('tools', detailsMode, sections),
|
||||
subagents: sectionMode('subagents', detailsMode, sections),
|
||||
activity: sectionMode('activity', detailsMode, sections)
|
||||
thinking: sectionMode('thinking', detailsMode, sections, commandOverride),
|
||||
tools: sectionMode('tools', detailsMode, sections, commandOverride),
|
||||
subagents: sectionMode('subagents', detailsMode, sections, commandOverride),
|
||||
activity: sectionMode('activity', detailsMode, sections, commandOverride)
|
||||
}),
|
||||
[detailsMode, sections]
|
||||
[commandOverride, detailsMode, sections]
|
||||
)
|
||||
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
|
|
|||
|
|
@ -57,9 +57,20 @@ export const resolveSections = (raw: unknown): SectionVisibility =>
|
|||
) as SectionVisibility)
|
||||
: {}
|
||||
|
||||
// Effective mode for one section: explicit override → SECTION_DEFAULTS → global.
|
||||
// Single source of truth for "is this section open by default / rendered at all".
|
||||
export const sectionMode = (name: SectionName, global: DetailsMode, sections?: SectionVisibility): DetailsMode =>
|
||||
sections?.[name] ?? SECTION_DEFAULTS[name] ?? global
|
||||
// Effective mode for one section: explicit override → global command mode →
|
||||
// built-in live-stream defaults → global config mode.
|
||||
//
|
||||
// The `commandOverride` flag is set for in-session `/details <mode>` changes.
|
||||
// That command should immediately apply to every section, including sections
|
||||
// with built-in defaults like thinking/tools=expanded and activity=hidden. On
|
||||
// startup/config sync we keep those defaults layered above the persisted global
|
||||
// config so the TUI still opens live reasoning/tools by default unless the user
|
||||
// pins explicit per-section overrides.
|
||||
export const sectionMode = (
|
||||
name: SectionName,
|
||||
global: DetailsMode,
|
||||
sections?: SectionVisibility,
|
||||
commandOverride = false
|
||||
): DetailsMode => sections?.[name] ?? (commandOverride ? global : (SECTION_DEFAULTS[name] ?? global))
|
||||
|
||||
export const nextDetailsMode = (m: DetailsMode): DetailsMode => MODES[(MODES.indexOf(m) + 1) % MODES.length]!
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue