Skip to content

feat(tui): customizable footer status line via status_line config - #2255

Merged
liruifengv merged 5 commits into
MoonshotAI:mainfrom
he-yufeng:feat/status-line-config
Jul 28, 2026
Merged

feat(tui): customizable footer status line via status_line config#2255
liruifengv merged 5 commits into
MoonshotAI:mainfrom
he-yufeng:feat/status-line-config

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

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_line item lists) and Claude Code (statusLine command with JSON on stdin) offer one.

What changed

Adds a [status_line] section to ~/.kimi-code/tui.toml covering 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 command is set it wins over items for 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. statusLine on AppState is 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) plus config.test.ts coverage 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

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

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-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 178ddb4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread apps/kimi-code/src/tui/config.ts Outdated
[upgrade]
auto_install = ${String(config.upgrade.autoInstall)} # true | false

# [status_line]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +114 to +117
if (line !== null) {
this.cached = line;
this.onUpdate();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@he-yufeng

Copy link
Copy Markdown
Contributor Author

Thanks for the sharp read. Addressed in 21a0c7e:

  • Round-trip on save (P1): real bug, and the most important one here. saveTuiConfig rewrites the whole file, so an active [status_line] was silently dropped by any unrelated preference save. The section is now rendered live when set (items and command both serialize) and stays a commented-out guide when unset, with a save/reload regression test.
  • Platform shell: spawns ComSpec/cmd.exe on Windows instead of assuming sh.exe.
  • Process tree on timeout: POSIX runs the command in its own process group and the timeout kills the group; Windows goes through taskkill /T /F. A script that spawned children can no longer leak them past the 300ms ceiling.
  • Reload application: landed in 02d31ce, already on this branch (applyReloadedTuiConfig pushes statusLine into AppState, so /reload-tui picks edits up live).

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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 28, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@178ddb4
npx https://pkg.pr.new/@moonshot-ai/kimi-code@178ddb4

commit: 178ddb4

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +238 to +240
if (this.statusLineRunner === null) {
this.statusLineRunner = new StatusLineCommandRunner(command, this.onRefresh);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +82 to +83
child.stdout?.on('data', (chunk: string) => {
stdout += chunk;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +297 to +300
// 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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +128 to +129
if (this.inFlight || now - this.lastRunAt < STATUS_LINE_RERUN_INTERVAL_MS) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread apps/kimi-code/src/tui/config.ts Outdated
Comment on lines +169 to +170
// oxlint-disable-next-line no-console
console.warn(`[tui.toml] ignoring unknown status_line item: ${item}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@liruifengv

Copy link
Copy Markdown
Collaborator

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.
@he-yufeng

Copy link
Copy Markdown
Contributor Author

All five addressed in 178ddb4, each with a regression test:

  • Runner lifecycle: a reload that swaps one non-empty command for another now disposes the old runner and creates a fresh one (previously the old script kept executing until restart). Covered by a test that flips printf "aaa" to printf "bbb" via setState and asserts the line follows.
  • Capture cap: accumulation stops as soon as the first line is complete, and a missing-newline stream is capped at 64KB. A 200KB single-line output now costs a bounded prefix instead of the whole stream every refresh.
  • Tips slot position: tips is a real composable slot now, so items = ["tips", "model"] renders the tip before the model instead of always pinning it to the far right. A test asserts the order flips with the configured position.
  • Trailing refresh: updates arriving inside the throttle window are deferred to a trailing run at interval expiry (and right after an in-flight run completes) rather than dropped, so a final state change always lands. Test counts actual command invocations past the interval.
  • Reload warnings: loadTuiConfig/parseTuiConfig/normalizeTuiConfig take an optional warn callback; the reload handlers pass host.showStatus(..., 'warning') so unknown-item warnings render in the status area instead of writing raw stderr over the active display.

Changeset text is tightened to one line as suggested. vitest on the two touched test files: 32/32 pass; oxlint and tsc clean.

@liruifengv liruifengv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: customizable status line (like Claude Code's /statusline)

3 participants