feat(proctitle): set dynamic terminal title with cwd + session topic#2083
Open
yeelam-gordon wants to merge 6 commits into
Open
feat(proctitle): set dynamic terminal title with cwd + session topic#2083yeelam-gordon wants to merge 6 commits into
yeelam-gordon wants to merge 6 commits into
Conversation
Mirrors what Copilot CLI and Claude Code do: keep the terminal tab/window title in sync with the live session so users with many tabs can identify each one at a glance. Format: 'Kimi Code [\u00b7 <topic>] [\u00b7 <cwd-basename>]'. The topic is the session's auto-generated or user-set title; it is clipped to 40 characters. Updates are intentionally low-frequency and tied to real state changes (NOT every keystroke or token): - Session created / found / continued (cli/__init__.py) - Auto-derived topic after the first turn (soul/kimisoul.py) - User runs /title (ui/shell/slash.py) Refs MoonshotAI#1475 (regression from v1.15.0 that set the title to a static 'Kimi Code'). Prior attempt MoonshotAI#1519 was closed without comment; this version uses the session topic as well as cwd, keeps the helper tested, and confines updates to well-defined hooks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…itize OSC payload - Route set_terminal_title through the pre-redirect stderr fd so /title and auto-topic refreshes keep working after redirect_stderr_to_logger swaps fd 2 for a pipe; cache the dup'd handle for the process lifetime to avoid os.dup + fdopen on every call. Falls back to sys.stderr only when the redirector isn't installed yet (early startup), and no-ops when no TTY is reachable so 'kimi --print' / piped runs stay quiet. (PR MoonshotAI#2083 reviews from Devin, Codex P1, Copilot) - Sanitize the composed title via _sanitize_osc_payload at the single chokepoint inside set_terminal_title: strip C0 (0x00-0x1f), DEL (0x7f), and C1 (0x80-0x9f) so that user-influenced topic and filesystem-derived cwd basenames cannot inject ESC/BEL/CR/LF and break out of the OSC sequence. Unicode (Chinese, Japanese, emoji, accented Latin) is preserved verbatim. (Codex P2, Copilot) - Narrow exception handlers in soul/kimisoul.py and ui/shell/slash.py from bare 'except Exception: pass' to 'except OSError' with a logger.opt(exception=True).debug carrying the session id. (Copilot) - Add get_original_stderr_handle public helper to utils/logging.py exposing the redirector's pre-redirect fd without needing the context-manager close semantics, so the proctitle cache can hold the handle long-term. - Tests: add coverage for the redirected-stderr fallback path, the early-startup fallback to sys.stderr, the non-TTY original-stderr no-op path, OSC control-byte sanitization, and Chinese / emoji round-trips through the OSC payload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two follow-ups from upstream review on PR MoonshotAI#2083: 1. The startup title refresh used 'session.state.custom_title or session.title or None', but Session.refresh() assigns the string 'Untitled' to empty sessions, so every fresh tab showed 'Kimi Code · Untitled · <cwd>'. Filter that placeholder out so the topic is omitted until a real one is generated. 2. The sys.stderr fallback in set_terminal_title only caught OSError. On legacy Windows code pages or ASCII/C locales, writing a Unicode topic (Chinese, emoji) raised UnicodeEncodeError mid-startup. Pre-encode through the stream's own encoding with errors='replace' and broaden the suppressed exceptions to (OSError, UnicodeError, LookupError) so title updates stay best-effort. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The two stderr-fallback tests set proctitle._original_stderr_attempted = True before running, but that attribute does not exist in the module — Python silently created it as a fresh attribute that nothing reads, making the reset a no-op. The autouse _reset_cached_handle fixture and the stubbed get_original_stderr_handle already provide the needed isolation, so the unused lines are dropped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The redirector returns the original-stderr fd with the inheritable flag set (os.set_inheritable(..., True)), and we cache that fd at module level for the lifetime of the process. Without intervention, every subprocess we later spawn (shell tools, MCP servers) inherits a TTY handle it should not own — a slow leak that also leaves the descriptor table dirty. Flip the flag on our cached copy via os.set_inheritable(fd, False) right after we accept the candidate. The redirector's own state is untouched; only the title-emission cache is locked down. Suppression of OSError + AttributeError keeps it safe on platforms (or replacement fd objects) where set_inheritable is unavailable. New unit test: simulate the redirector returning an inheritable pipe fd, call _get_original_stderr_handle(), assert os.get_inheritable(fd) is False. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1a295eb1a1
ℹ️ 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".
Session.create / find / continue_ canonicalize the work_dir but the startup title call still used the raw CLI 'work_dir' argument. Running 'kimi --work-dir .' (or '..') therefore produced tab titles like 'Kimi Code . .' instead of 'Kimi Code . my-project', regressing the multi-tab disambiguation that motivates this feature for relative --work-dir usage. Switching to 'session.work_dir' (already canonicalized) gives the actual project basename in every entry path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Apr 28, 2026
yeelam-gordon
pushed a commit
to yeelam-gordon/kimi-cli
that referenced
this pull request
Apr 28, 2026
…only Tighten PR MoonshotAI#2082 to a single concern: write a runtime.json sidecar so external tools can map a PID to a session id. The setproctitle helpers (compose_session_process_title, set_session_process_title, _short_session_id, _sanitize_proctitle_token) and the OS-process-title reset on /web /vis switch are removed entirely — they were a parallel-mechanism for the same observability goal that complicated the diff and overlapped scope with the still-open setproctitle conversation. Removes: - All session-aware helpers from utils/proctitle.py (file is restored to main) - The set_session_process_title call after session establishment in cli/__init__.py - The set_process_title('Kimi Code') reset in the outer finally - tests/utils/test_proctitle.py (was solely testing the now-removed helpers; the OSC tab-title tests live in PR MoonshotAI#2083 separately) Keeps: - runtime_status module + tests (the actual PR scope) - The runtime.json write at session establishment - The targeted clear_runtime_status on SwitchToWeb / SwitchToVis (still needed because the PID stays alive) External consumers that have a PID can read the runtime.json files in session dirs and check the recorded PID against the live process table — no proctitle channel needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
yeelam-gordon
pushed a commit
to yeelam-gordon/kimi-cli
that referenced
this pull request
Apr 28, 2026
…only Tighten PR MoonshotAI#2082 to a single concern: write a runtime.json sidecar so external tools can map a PID to a session id. The setproctitle helpers (compose_session_process_title, set_session_process_title, _short_session_id, _sanitize_proctitle_token) and the OS-process-title reset on /web /vis switch are removed entirely — they were a parallel-mechanism for the same observability goal that complicated the diff and overlapped scope with the still-open setproctitle conversation. Removes: - All session-aware helpers from utils/proctitle.py (file is restored to main) - The set_session_process_title call after session establishment in cli/__init__.py - The set_process_title('Kimi Code') reset in the outer finally - tests/utils/test_proctitle.py (was solely testing the now-removed helpers; the OSC tab-title tests live in PR MoonshotAI#2083 separately) Keeps: - runtime_status module + tests (the actual PR scope) - The runtime.json write at session establishment - The targeted clear_runtime_status on SwitchToWeb / SwitchToVis (still needed because the PID stays alive) External consumers that have a PID can read the runtime.json files in session dirs and check the recorded PID against the live process table — no proctitle channel needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5 tasks
This was referenced Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Resolves #1475 (regression from v1.15.0). Prior attempt #1519 was
closed without merge; this version differs by also surfacing the
session topic (not just cwd), which is what disambiguates tabs
when the same project has multiple concurrent sessions.
Problem
Since v1.15.0 (#1254) the terminal tab/window title has been a
static
Kimi Codestring. Users with multiple sessions acrossmultiple tabs cannot tell which session belongs to which tab.
Copilot CLI and Claude Code both keep their tab title in sync with
the live session.
Solution
Format:
Kimi Code · <topic> · <cwd-basename>Topic comes from the session's auto-generated title (derived from
the first user message) or the user-set
/titlevalue, clipped to40 characters with an ellipsis. cwd basename is taken from the
canonicalised
session.work_dirsokimi --work-dir .stillgives a useful project name.
Examples:
Kimi Code · my-projectKimi Code · Refactor auth module to use JWT · my-project/title Q4 planning:Kimi Code · Q4 planning · my-projectUpdate points
Three lifecycle hooks, no flapping on every token / tool call:
cli/__init__.py— after session establishment.soul/kimisoul.py— after the first-turn auto-title is derived.ui/shell/slash.py— on/title.Implementation hardening
set_terminal_titleroutes through the pre-redirect stderr fdvia
utils.logging.get_original_stderr_handle, so refreshes keepworking after
redirect_stderr_to_loggerswaps fd 2 for a pipe.Falls back to
sys.stderronly during early startup; no-opscleanly when no TTY is reachable so
--printand pipes stayclean.
_sanitize_osc_payloadstrips C0/C1 controls (\x00-\x1f,\x7f-\x9f) at a single chokepoint so user-influenced topic /cwd cannot inject terminal escape codes. All other Unicode is
preserved verbatim.
sys.stderrfallback pre-encodes through the stream's encodingwith
errors='replace', so legacy Windows code pages or ASCII /C locales don't crash on Chinese / emoji titles.
subprocesses (shell tools, MCP servers) don't leak a TTY handle.
Files
src/kimi_cli/utils/proctitle.py(helpers + sanitiser)src/kimi_cli/utils/logging.py(get_original_stderr_handlehelper)src/kimi_cli/cli/__init__.py,soul/kimisoul.py,ui/shell/slash.py(the three update points)tests/utils/test_proctitle.py(25 tests: composition,truncation, OSC emission, TTY gating, redirected-stderr fallback,
encoding-safe fallback, non-inheritable fd, control-char sanitiser,
Chinese / emoji round-trip)
Out of scope
OS process title via
setproctitle, runtime.json sidecar(separate PR #2082).
Checklist