diff --git a/tui_gateway/server.py b/tui_gateway/server.py index dfc158076..32bac4ffa 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -455,18 +455,22 @@ def _write_config_key(key_path: str, value): _save_cfg(cfg) -# Legacy configs stored display.tui_statusbar as a bool. Coerce both bool and -# string forms to the string enum so rollouts don't require a manual migration. +# Legacy configs stored display.tui_statusbar as a bool; a short-lived +# intermediate wrote 'on'. Both forms map to 'top' — the inline position +# above the input where the bar originally lived — so users don't need to +# migrate by hand. def _coerce_statusbar(raw) -> str: if raw is True: - return "on" + return "top" if raw is False: return "off" if isinstance(raw, str): s = raw.strip().lower() - if s in {"on", "off", "bottom", "top"}: + if s == "on": + return "top" + if s in {"off", "top", "bottom"}: return s - return "on" + return "top" def _load_reasoning_config() -> dict | None: @@ -2533,10 +2537,12 @@ def _(rid, params: dict) -> dict: raw = str(value or "").strip().lower() cfg0 = _load_cfg() d0 = cfg0.get("display") if isinstance(cfg0.get("display"), dict) else {} - current = _coerce_statusbar(d0.get("tui_statusbar", "on")) + current = _coerce_statusbar(d0.get("tui_statusbar", "top")) if raw in ("", "toggle"): - nv = "on" if current == "off" else "off" - elif raw in ("on", "off", "bottom", "top"): + nv = "top" if current == "off" else "off" + elif raw == "on": + nv = "top" + elif raw in ("off", "top", "bottom"): nv = raw else: return _err(rid, 4002, f"unknown statusbar value: {value}") @@ -2659,7 +2665,7 @@ def _(rid, params: dict) -> dict: on = bool(_load_cfg().get("display", {}).get("tui_compact", False)) return _ok(rid, {"value": "on" if on else "off"}) if key == "statusbar": - raw = _load_cfg().get("display", {}).get("tui_statusbar", "on") + raw = _load_cfg().get("display", {}).get("tui_statusbar", "top") return _ok(rid, {"value": _coerce_statusbar(raw)}) if key == "mtime": cfg_path = _hermes_home / "config.yaml" diff --git a/ui-tui/src/__tests__/useConfigSync.test.ts b/ui-tui/src/__tests__/useConfigSync.test.ts index 09b21b14c..b5b25ddd8 100644 --- a/ui-tui/src/__tests__/useConfigSync.test.ts +++ b/ui-tui/src/__tests__/useConfigSync.test.ts @@ -40,6 +40,16 @@ describe('applyDisplay', () => { expect(s.streaming).toBe(false) }) + it('coerces legacy true + "on" alias to top', () => { + const setBell = vi.fn() + + applyDisplay({ config: { display: { tui_statusbar: true as unknown as 'on' } } }, setBell) + expect($uiState.get().statusBar).toBe('top') + + applyDisplay({ config: { display: { tui_statusbar: 'on' } } }, setBell) + expect($uiState.get().statusBar).toBe('top') + }) + it('applies v1 parity defaults when display fields are missing', () => { const setBell = vi.fn() @@ -50,7 +60,7 @@ describe('applyDisplay', () => { expect(s.inlineDiffs).toBe(true) expect(s.showCost).toBe(false) expect(s.showReasoning).toBe(false) - expect(s.statusBar).toBe('on') + expect(s.statusBar).toBe('top') expect(s.streaming).toBe(true) }) @@ -77,22 +87,22 @@ describe('applyDisplay', () => { }) describe('normalizeStatusBar', () => { - it('maps legacy bool to on/off', () => { - expect(normalizeStatusBar(true)).toBe('on') + it('maps legacy bool + on alias to top/off', () => { + expect(normalizeStatusBar(true)).toBe('top') expect(normalizeStatusBar(false)).toBe('off') + expect(normalizeStatusBar('on')).toBe('top') }) - it('passes through the new string enum', () => { - expect(normalizeStatusBar('on')).toBe('on') + it('passes through the canonical enum', () => { expect(normalizeStatusBar('off')).toBe('off') - expect(normalizeStatusBar('bottom')).toBe('bottom') expect(normalizeStatusBar('top')).toBe('top') + expect(normalizeStatusBar('bottom')).toBe('bottom') }) - it('defaults missing/unknown values to on', () => { - expect(normalizeStatusBar(undefined)).toBe('on') - expect(normalizeStatusBar(null)).toBe('on') - expect(normalizeStatusBar('sideways')).toBe('on') - expect(normalizeStatusBar(42)).toBe('on') + it('defaults missing/unknown values to top', () => { + expect(normalizeStatusBar(undefined)).toBe('top') + expect(normalizeStatusBar(null)).toBe('top') + expect(normalizeStatusBar('sideways')).toBe('top') + expect(normalizeStatusBar(42)).toBe('top') }) }) diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index e44cdc060..c1c427739 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -27,7 +27,7 @@ export interface StateSetter { (value: SetStateAction): void } -export type StatusBarMode = 'bottom' | 'off' | 'on' | 'top' +export type StatusBarMode = 'bottom' | 'off' | 'top' export interface SelectionApi { clearSelection: () => void diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 13672c0d1..a418e28ac 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -8,10 +8,10 @@ import type { SessionSteerResponse, SessionUndoResponse } from '../../../gatewayTypes.js' -import type { StatusBarMode } from '../../interfaces.js' import { writeOsc52Clipboard } from '../../../lib/osc52.js' import { configureDetectedTerminalKeybindings, configureTerminalKeybindings } from '../../../lib/terminalSetup.js' import type { DetailsMode, Msg, PanelSection } from '../../../types.js' +import type { StatusBarMode } from '../../interfaces.js' import { patchOverlayState } from '../../overlayStore.js' import { patchUiState } from '../../uiStore.js' import type { SlashCommand } from '../types.js' @@ -306,24 +306,28 @@ export const coreCommands: SlashCommand[] = [ { aliases: ['sb'], - help: 'status bar position (on|off|bottom|top)', + help: 'status bar position (on|off|top|bottom)', name: 'statusbar', run: (arg, ctx) => { const mode = arg.trim().toLowerCase() const current = ctx.ui.statusBar - // No-arg / `toggle` flips visibility while preserving the last - // explicit position: off → on (inline default), any-visible → off. + + // 'on' is a legacy alias for 'top' — the inline position above the + // input where the bar originally lived. No-arg / `toggle` flips + // visibility and defaults to 'top' when reappearing. const next: null | StatusBarMode = mode === '' || mode === 'toggle' ? current === 'off' - ? 'on' + ? 'top' : 'off' - : mode === 'on' || mode === 'off' || mode === 'bottom' || mode === 'top' - ? mode - : null + : mode === 'on' || mode === 'top' + ? 'top' + : mode === 'off' || mode === 'bottom' + ? mode + : null if (next === null) { - return ctx.transcript.sys('usage: /statusbar [on|off|bottom|top|toggle]') + return ctx.transcript.sys('usage: /statusbar [on|off|top|bottom|toggle]') } patchUiState({ statusBar: next }) diff --git a/ui-tui/src/app/uiStore.ts b/ui-tui/src/app/uiStore.ts index 761c2d074..fcf2e5d88 100644 --- a/ui-tui/src/app/uiStore.ts +++ b/ui-tui/src/app/uiStore.ts @@ -16,7 +16,7 @@ const buildUiState = (): UiState => ({ showReasoning: false, sid: null, status: 'summoning hermes…', - statusBar: 'on', + statusBar: 'top', streaming: true, theme: DEFAULT_THEME, usage: ZERO diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts index ac363e4dd..a8f64c3a5 100644 --- a/ui-tui/src/app/useConfigSync.ts +++ b/ui-tui/src/app/useConfigSync.ts @@ -14,17 +14,18 @@ import type { StatusBarMode } from './interfaces.js' import { turnController } from './turnController.js' import { patchUiState } from './uiStore.js' -const STATUSBAR_MODES = new Set(['bottom', 'off', 'on', 'top']) +const STATUSBAR_MODES = new Set(['bottom', 'off', 'top']) -// Legacy configs stored tui_statusbar as a bool; new configs write a string -// ('on' | 'off' | 'bottom' | 'top'). Coerce both shapes so existing users -// keep their preference without manual migration. +// Legacy configs stored tui_statusbar as a bool; the short-lived 4-mode +// variant wrote 'on'. Both map to 'top' (inline above the input) — the +// original feature's default — so users keep their preference without +// manual migration. export const normalizeStatusBar = (raw: unknown): StatusBarMode => { if (raw === false) return 'off' - if (raw === true || raw == null) return 'on' + if (raw === true || raw == null || raw === 'on') return 'top' if (typeof raw === 'string' && STATUSBAR_MODES.has(raw as StatusBarMode)) return raw as StatusBarMode - return 'on' + return 'top' } const MTIME_POLL_MS = 5000 diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 517a65c6e..d2607bad2 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -184,7 +184,7 @@ const ComposerPane = memo(function ComposerPane({ )} - + & { at: 'bottom' | 'on' | 'top' }) { +}: Pick & { at: 'bottom' | 'top' }) { const ui = useStore($uiState) if (ui.statusBar !== at) { @@ -300,8 +300,6 @@ export const AppLayout = memo(function AppLayout({ return ( - {!overlay.agents && } - {overlay.agents ? (