diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 32bac4ffa..49cd9660b 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -455,21 +455,14 @@ def _write_config_key(key_path: str, value): _save_cfg(cfg) -# 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. +_STATUSBAR_MODES = frozenset({"off", "top", "bottom"}) + + def _coerce_statusbar(raw) -> str: - if raw is True: - return "top" if raw is False: return "off" - if isinstance(raw, str): - s = raw.strip().lower() - if s == "on": - return "top" - if s in {"off", "top", "bottom"}: - return s + if isinstance(raw, str) and (s := raw.strip().lower()) in _STATUSBAR_MODES: + return s return "top" @@ -2535,17 +2528,18 @@ def _(rid, params: dict) -> dict: if key == "statusbar": raw = str(value or "").strip().lower() - cfg0 = _load_cfg() - d0 = cfg0.get("display") if isinstance(cfg0.get("display"), dict) else {} + d0 = _load_cfg().get("display") or {} current = _coerce_statusbar(d0.get("tui_statusbar", "top")) + if raw in ("", "toggle"): nv = "top" if current == "off" else "off" elif raw == "on": nv = "top" - elif raw in ("off", "top", "bottom"): + elif raw in _STATUSBAR_MODES: nv = raw else: return _err(rid, 4002, f"unknown statusbar value: {value}") + _write_config_key("display.tui_statusbar", nv) return _ok(rid, {"key": key, "value": nv}) diff --git a/ui-tui/packages/hermes-ink/src/ink/wrap-text.ts b/ui-tui/packages/hermes-ink/src/ink/wrap-text.ts index c0b95df08..e8290feac 100644 --- a/ui-tui/packages/hermes-ink/src/ink/wrap-text.ts +++ b/ui-tui/packages/hermes-ink/src/ink/wrap-text.ts @@ -50,17 +50,8 @@ export default function wrapText(text: string, maxWidth: number, wrapType: Style }) } - // Char-granularity wrap: break at exact column boundaries regardless of - // whitespace. Used for text inputs where the cursor position must track - // the wrap boundary deterministically — word-wrap's whitespace-preferring - // reshuffle causes visible cursor flicker as each keystroke can push a - // word across a line break. if (wrapType === 'wrap-char') { - return wrapAnsi(text, maxWidth, { - trim: false, - hard: true, - wordWrap: false - }) + return wrapAnsi(text, maxWidth, { trim: false, hard: true, wordWrap: false }) } if (wrapType === 'wrap-trim') { diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index a418e28ac..904882c21 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -310,23 +310,18 @@ export const coreCommands: SlashCommand[] = [ name: 'statusbar', run: (arg, ctx) => { const mode = arg.trim().toLowerCase() - const current = ctx.ui.statusBar + const toggle: StatusBarMode = ctx.ui.statusBar === 'off' ? 'top' : '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' - ? 'top' - : 'off' + !mode || mode === 'toggle' + ? toggle : mode === 'on' || mode === 'top' ? 'top' : mode === 'off' || mode === 'bottom' ? mode : null - if (next === null) { + if (!next) { return ctx.transcript.sys('usage: /statusbar [on|off|top|bottom|toggle]') } diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts index a8f64c3a5..f50dcbd10 100644 --- a/ui-tui/src/app/useConfigSync.ts +++ b/ui-tui/src/app/useConfigSync.ts @@ -16,17 +16,12 @@ import { patchUiState } from './uiStore.js' const STATUSBAR_MODES = new Set(['bottom', 'off', 'top']) -// 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 || raw === 'on') return 'top' - if (typeof raw === 'string' && STATUSBAR_MODES.has(raw as StatusBarMode)) return raw as StatusBarMode - - return 'top' -} +export const normalizeStatusBar = (raw: unknown): StatusBarMode => + raw === false + ? 'off' + : typeof raw === 'string' && STATUSBAR_MODES.has(raw as StatusBarMode) + ? (raw as StatusBarMode) + : 'top' const MTIME_POLL_MS = 5000 diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index bb88383ae..07f241cb5 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -378,10 +378,7 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { return cActions.openEditor() } - // Shift-Tab toggles per-session yolo without submitting a turn — mirrors - // Claude Code's in-place dangerously-approve toggle. Slash /yolo keeps - // working for discoverability; this just skips the inference round-trip - // when you only want to flip the flag mid-flow (blitz #5 sub-item 11). + // shift-tab flips yolo without spending a turn (claude-code parity) if (key.shift && key.tab && !cState.completions.length) { return void gateway .rpc('config.set', { key: 'yolo', session_id: live.sid }) diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 439afd962..b9b5e9450 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -191,7 +191,7 @@ export function StatusRule({ const leftWidth = Math.max(12, cols - cwdLabel.length - 3) return ( - + {'─ '} diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 171ee27f5..c96960ac8 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -216,13 +216,7 @@ const ComposerPane = memo(function ComposerPane({ - {/* - Subtract the NoSelect paddingX={1} (2 cols total) and the - prompt-glyph column (pw) so cursorLayout agrees with the - width wrap-ansi actually uses at render time. Off-by-one/ - two here manifests as the final letter flickering - in/out when a sentence crosses the wrap boundary. - */} + {/* subtract NoSelect paddingX={1} (2 cols) + pw so wrap-ansi and cursorLayout agree */} + {!overlay.agents && ( - + <> + + + + + + )} - - {!overlay.agents && } - - {!overlay.agents && } ) diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index 4b6950cf5..d17631cfe 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -167,11 +167,8 @@ export function lineNav(s: string, p: number, dir: -1 | 1): null | number { return snapPos(s, Math.min(nextBreak + 1 + col, lineEnd)) } -// Cursor layout mirrors `wrap-ansi(text, cols, { wordWrap: false, hard: true })` -// which is what `` ends up feeding to the renderer. -// Char-granularity wrap keeps wrap boundaries deterministic as the user -// types — word-wrap's whitespace-preferring reshuffle causes the cursor -// to flicker as each keystroke moves a word across a line break (blitz #9). +// mirrors wrap-ansi(..., { wordWrap: false, hard: true }) so the declared +// cursor lines up with what actually renders export function cursorLayout(value: string, cursor: number, cols: number) { const pos = Math.max(0, Math.min(cursor, value.length)) const w = Math.max(1, cols) @@ -205,11 +202,7 @@ export function cursorLayout(value: string, cursor: number, cols: number) { col += sw } - // The cursor renders as an inverted cell AFTER the character at `pos` - // (or as a standalone trailing cell when `pos === value.length`). If - // col has reached the wrap column, that cell overflows to the next row - // — match wrap-ansi's behavior so the declared cursor doesn't sit past - // the visual edge. + // trailing cursor-cell overflows to the next row at the wrap column if (col >= w) { line++ col = 0