Skip to content

feat(proctitle): set dynamic terminal title with cwd + session topic#2083

Open
yeelam-gordon wants to merge 6 commits into
MoonshotAI:mainfrom
yeelam-gordon:feat/dynamic-terminal-title
Open

feat(proctitle): set dynamic terminal title with cwd + session topic#2083
yeelam-gordon wants to merge 6 commits into
MoonshotAI:mainfrom
yeelam-gordon:feat/dynamic-terminal-title

Conversation

@yeelam-gordon

@yeelam-gordon yeelam-gordon commented Apr 27, 2026

Copy link
Copy Markdown

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 Code string. Users with multiple sessions across
multiple 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 /title value, clipped to
40 characters with an ellipsis. cwd basename is taken from the
canonicalised session.work_dir so kimi --work-dir . still
gives a useful project name.

Examples:

  • Fresh session: Kimi Code · my-project
  • After first turn: Kimi Code · Refactor auth module to use JWT · my-project
  • After /title Q4 planning: Kimi Code · Q4 planning · my-project

Update 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_title routes through the pre-redirect stderr fd
    via utils.logging.get_original_stderr_handle, so refreshes keep
    working after redirect_stderr_to_logger swaps fd 2 for a pipe.
    Falls back to sys.stderr only during early startup; no-ops
    cleanly when no TTY is reachable so --print and pipes stay
    clean.
  • _sanitize_osc_payload strips 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.stderr fallback pre-encodes through the stream's encoding
    with errors='replace', so legacy Windows code pages or ASCII /
    C locales don't crash on Chinese / emoji titles.
  • The cached pre-redirect fd is marked non-inheritable so spawned
    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_handle helper)
  • 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

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>
Copilot AI review requested due to automatic review settings April 27, 2026 01:32
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

…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>
chatgpt-codex-connector[bot]

This comment was marked as resolved.

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>
devin-ai-integration[bot]

This comment was marked as resolved.

This comment was marked as resolved.

yeelam-gordon and others added 2 commits April 27, 2026 10:23
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>

@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: 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".

Comment thread src/kimi_cli/cli/__init__.py Outdated
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>
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>
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: Option to display current directory in prompt or window title (regression from v1.15.0)

3 participants