feat(tui): customizable footer status line via status_line config - #2255
Conversation
The bottom status bar was a fixed layout. Add a [status_line] section to tui.toml covering the two established models: - items: codex-style composition. Pick and order the built-in slots (mode, goal, model, tasks, cwd, git, tips); unset keeps today's layout, unknown ids are skipped with a warning, and an empty list blanks line 1. - command: claude-code-style custom line. The footer runs the command with a JSON snapshot on stdin (model, cwd, git branch, permission and plan mode, context usage, session id, version) and renders the first stdout line. Runs are throttled to one per second and capped at 300ms; nonzero exit, empty output, or a timeout falls back to the built-in layout. Line 2 (context readout) stays built-in in every mode. Resolve MoonshotAI#2116.
🦋 Changeset detectedLatest commit: 178ddb4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The reload command pushes reloaded tui.toml fields into AppState; the new statusLine field joins that list so edits go live without a restart. The config files reference (EN/ZH) documents the new section.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3778559c66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| [upgrade] | ||
| auto_install = ${String(config.upgrade.autoInstall)} # true | false | ||
|
|
||
| # [status_line] |
There was a problem hiding this comment.
Serialize the active status-line config on saves
When a user has an active [status_line] section and changes another preference through /theme, /editor, or the update settings, those flows call saveTuiConfig, but renderTuiConfig emits only this commented example and never serializes config.statusLine. The rewrite therefore silently deletes the user's items and command settings; render the active section when configured so unrelated preference changes preserve it.
Useful? React with 👍 / 👎.
| disablePasteBurst: input.tuiConfig.disablePasteBurst, | ||
| notifications: input.tuiConfig.notifications, | ||
| upgrade: input.tuiConfig.upgrade, | ||
| statusLine: input.tuiConfig.statusLine, |
There was a problem hiding this comment.
Apply status-line changes during TUI config reloads
This copies statusLine only during initial state creation. Both /reload-tui and /reload load the updated tui.toml through applyReloadedTuiConfig, but its setAppState patch in src/tui/commands/reload.ts omits statusLine, so editing either items or command and invoking the TUI reload command leaves the old footer configuration active until the process is restarted.
Useful? React with 👍 / 👎.
|
|
||
| let child; | ||
| try { | ||
| child = spawn('sh', ['-c', command], { |
There was a problem hiding this comment.
Use the platform shell for status-line commands
On supported native Windows installations where a POSIX sh.exe is not independently present on PATH, every configured status-line command fails with ENOENT and silently falls back to the built-in footer. The repository's other user-command paths use the system shell or explicitly resolve the Windows shell, so this runner should do the same instead of hard-coding sh.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| const timer = setTimeout(() => { | ||
| child.kill('SIGKILL'); |
There was a problem hiding this comment.
Kill the whole command tree on timeout
When the configured shell command starts a child process—for example a script, pipeline, or background job—killing only the sh process at 300 ms can leave its descendants running. Repeated footer refreshes can consequently accumulate orphaned status-line processes despite the advertised timeout; launch the command in an isolated process group and terminate the tree, with the corresponding Windows handling.
Useful? React with 👍 / 👎.
| if (line !== null) { | ||
| this.cached = line; | ||
| this.onUpdate(); | ||
| } |
There was a problem hiding this comment.
Clear cached output when a refresh fails
If a status-line command succeeds once and a later invocation times out, exits nonzero, or returns empty output, line becomes null but this branch leaves the previous cached line intact. The footer therefore displays stale custom output indefinitely instead of falling back to the built-in layout as specified; update the cache and request a repaint when a failed refresh invalidates a previously successful value.
Useful? React with 👍 / 👎.
Codex review on MoonshotAI#2255 caught a real one: saveTuiConfig rewrites the whole tui.toml, so changing any other preference dropped an active [status_line] section. Render it live when set (items and command), commented-out guide when unset. Also: spawn the command through ComSpec/cmd.exe on Windows instead of assuming sh.exe, and take the whole process tree down on timeout (process-group kill on POSIX, taskkill /T on Windows) so a script that spawned children cannot leak them.
|
Thanks for the sharp read. Addressed in 21a0c7e:
On clearing the cached line after a failed refresh, I kept the last good line deliberately: the runner reruns every second, so a transient failure would otherwise flicker the footer between the custom line and the built-in layout. A command that never succeeds shows the built-in layout from the start, which is the failure mode that actually needs the fallback. Happy to switch it to clear-on-error if you'd rather have hard consistency there. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e705cffab7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (this.statusLineRunner === null) { | ||
| this.statusLineRunner = new StatusLineCommandRunner(command, this.onRefresh); | ||
| } |
There was a problem hiding this comment.
Recreate the runner when the command changes
When a user replaces one non-empty status_line.command with another and invokes /reload-tui or /reload, this branch retains the runner created for the old command, so the footer continues executing the previous script until restart. Although reload.ts now propagates statusLine into app state, this new evidence shows that the reload remains incomplete because the runner never compares or updates its stored command.
Useful? React with 👍 / 👎.
| child.stdout?.on('data', (chunk: string) => { | ||
| stdout += chunk; |
There was a problem hiding this comment.
Bound the captured status-command output
When a configured command emits a large or unending stdout stream, every chunk is appended even though only the first line is used. The 300 ms timeout limits runtime but not output volume, so a command such as yes can allocate tens of megabytes on every refresh and cause repeated memory spikes; stop after the first newline or enforce a small output cap.
Useful? React with 👍 / 👎.
| // Rotating hint tips stay on the right unless the user dropped 'tips' | ||
| // from items (or a command took the line, handled above). | ||
| let tipText = ''; | ||
| const showTips = configured === null || configured.includes('tips'); |
There was a problem hiding this comment.
Honor the configured position of the tips slot
When items places tips anywhere except last, its configured position is ignored: the composition loop has no tips entry, and this separate includes check always renders tips at the far right. For example, items = ["tips", "model"] displays the model before the tips despite the documented ordered-slot contract.
Useful? React with 👍 / 👎.
| if (this.inFlight || now - this.lastRunAt < STATUS_LINE_RERUN_INTERVAL_MS) { | ||
| return; |
There was a problem hiding this comment.
Schedule a trailing refresh after throttling
When footer state changes within one second of the previous command run, this early return drops the refresh instead of deferring it until the interval expires. If that state update is the last UI event—for example, an idle user toggles Plan mode shortly after a run—the cached custom line can show the old payload indefinitely; retain the newest payload and schedule a trailing refresh.
Useful? React with 👍 / 👎.
| // oxlint-disable-next-line no-console | ||
| console.warn(`[tui.toml] ignoring unknown status_line item: ${item}`); |
There was a problem hiding this comment.
Route reload warnings through the TUI
When /reload-tui or /reload encounters an unknown status_line.items value, this writes directly to stderr while the interactive renderer is active. That bypasses pi-tui's tracked cursor and output state, so the warning can corrupt or displace the current display; return the diagnostic to the command handler and show it through the TUI status surface instead.
Useful? React with 👍 / 👎.
|
Thank you for your contribution. The comments in Codex Review need to be addressed. Also, the Changeset text could be made more concise. |
…tips slot - recreate the command runner when a reload swaps status_line.command; the old runner kept executing the previous script until restart - schedule a trailing refresh instead of dropping updates that arrive inside the throttle window, so the last state change always lands - stop accumulating stdout once the first line is complete (and cap a missing-newline stream at 64KB); only the first line is ever rendered - honor the configured position of the tips slot in items instead of always pinning tips to the far right - route unknown status_line.items warnings through the TUI status area on reload instead of raw stderr, which could corrupt the display Changeset text tightened per maintainer note.
|
All five addressed in 178ddb4, each with a regression test:
Changeset text is tightened to one line as suggested. |
Related Issue
Resolve #2116
Problem
See linked issue. The bottom status bar was a fixed layout with no user customization, while both Codex (
tui.status_lineitem lists) and Claude Code (statusLinecommand with JSON on stdin) offer one.What changed
Adds a
[status_line]section to~/.kimi-code/tui.tomlcovering both models:items: codex-style composition. Pick and order the built-in slots (mode,goal,model,tasks,cwd,git,tips). Unset keeps today's exact layout, so nothing changes for existing users. Unknown ids are skipped with a warning instead of failing the whole config, and an empty list blanks line 1.command: claude-code-style custom line. The footer runs the command with a JSON snapshot on stdin (model, cwd, git branch, permission mode, plan mode, context usage, session id, version) and renders the first stdout line. Runs are throttled to one per second and time-boxed at 300ms (Claude Code's ceiling); nonzero exit, empty output, or a timeout falls back to the built-in layout, so a broken script never takes the footer down with it.When
commandis set it wins overitemsfor line 1. Line 2 (context readout) stays built-in in every mode.The footer render was refactored into slot builders (
buildSlots) so composition and the default path share one code path.statusLineonAppStateis optional so pre-existing fixtures keep typechecking.Tests: 12 new tests in
footer-status-line.test.ts(slot order/hiding, tips opt-out, empty items, JSON stdin shape, first-line-only, nonzero exit, timeout, runner caching, command fallback) plusconfig.test.tscoverage for the new section (parse, unknown item warning, empty command, rendered template). Full suite 2401 passed, typecheck and oxlint clean. Changeset included (minor,@moonshot-ai/kimi-code).Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.