Skip to content

feat: opt-in conversation tab titles - #14

Merged
javiermolinar merged 9 commits into
javiermolinar:mainfrom
chenzier:feat/conversation-tab-titles
Sep 28, 2026
Merged

javiermolinar merged 9 commits into
javiermolinar:mainfrom
chenzier:feat/conversation-tab-titles

Conversation

@chenzier

@chenzier chenzier commented Aug 22, 2026 •

Copy link
Copy Markdown

What

Conversation-driven tab titles, on by default. After the first agent turn of a conversation, the current cmux tab is renamed to a short (2-5 word) topic title, generated by a single in-process LLM call that reuses the session's model, auth, and model registry.

  • On by default; opt out with PI_CMUX_AUTOTITLE_DISABLED=1 or "pi-cmux": { "autotitle": false } in Pi settings (project-local .pi/settings.json wins over the global file).
  • PI_CMUX_AUTOTITLE_MODEL optionally selects the summarizer model (resolved via the model registry; falls back to the current model).
  • Tab-only: the workspace name is never touched, so this composes with cmux's built-in workspace auto-naming (which is skipped entirely for workspaces with a user-set name, and always renames the workspace itself).
  • /name overrides the automatic title; /new clears it so the next conversation gets a fresh one.
  • Runs after the turn ends, off the critical path; one summarizer call per conversation, aborted if the user starts a new turn first.

Why

pi-cmux already owns tab naming for the splits it spawns (renameSurfaceTab), but the tab hosting the current session keeps the default π - <folder> title forever. cmux's native auto-naming (automation.workspaceAutoNaming) cannot fill this gap for users who set workspace names: the spawn gate skips user-owned workspaces, and when it does run it renames the workspace (window title) as well.

Enabled by default because opt-in features stay undiscovered — users who never read the README never meet them. The escape hatches above make it safe to default on.

Implementation notes

The title is generated with one standalone completeSimple call (pi-ai's completion primitive, the same one compaction's completeSummarization wraps): a compact transcript (last 4 messages, each compressed to ~300 chars with a head/tail split around an ellipsis — requests and outcomes live at message edges, detail in the middle) with a short system prompt, model resolved from ctx.modelRegistry, auth from getApiKeyAndHeaders, maxTokens capped, cacheRetention: "none", and an AbortController with a timeout. Input for the naming request is ~1k tokens even for resumed long sessions.

An earlier iteration used a headless pi --print --no-tools child. That approach was rejected for three reasons, each a real failure mode:

  1. pi -r pollution — the headless child writes a real session file, so every titled conversation leaves a junk entry in the resume list.
  2. cmux notification storm — the child loads this package (and the host's cmux hooks) again; the naming pass itself re-triggered cmux notify. Env-var kill switches only cover extensions that know about them.
  3. Token cost — the child pays the full pi system prompt (AGENTS.md, skills, tool schemas) for what is a ~50-token answer.

The in-process call has none of these: no session file, no re-loaded extensions, and only the compact transcript is sent.

Failure budget: any failure (model resolution, auth, network, empty output, lost race with a closed tab) leaves state untouched so the next agent_end retries; nothing surfaces to the user and nothing blocks the conversation.

Verification (done locally against cmux 0.64.22 / pi 0.83.0 on macOS)

  • Live-tested end to end in cmux: first turn renames the tab; /new re-titles the next conversation; /name wins; tab-only, workspace untouched.
  • pi -r stays clean (no summarizer session files); cmux notifications fire only for the main conversation.
  • Extension loads cleanly under real pi (headless pi --print smoke test); config precedence verified unit-style (env force-on, env opt-out, settings true/false, bad JSON ignored).
  • Repo typecheck script passes (tsc --noEmit ... --strict extensions/*.ts) against @earendil-works/pi-coding-agent 0.83.0 (devDependency bumped to match; adds @earendil-works/pi-ai at the same version).

Docs

README config table + usage paragraph, CHANGELOG [Unreleased] entry.

wangchen01 added 7 commits September 9, 2026 16:25
After the first agent turn of a conversation, rename the current cmux tab
to a 2-5 word topic title summarized by a headless pi --print --no-tools
run. Tab-only (never touches the workspace name), /name overrides the
automatic title, /new resets it.

Disabled by default. Three guards keep the headless summarizer safe:
a ctx.mode gate so headless children never re-enter the naming pass, a
sanitized child env (CMUX_SURFACE_ID/CMUX_TAB_ID/CMUX_PANEL_ID removed,
CMUX_PI_HOOKS_DISABLED=1) so it cannot re-enter cmux hooks, and ignored
stdin because headless pi waits for stdin EOF.
A headless `pi --print` summarizer had three costs: it wrote a real
session file (polluting `pi -r`), it re-loaded extensions and re-fired
cmux notifications for the naming pass itself, and it paid the full pi
system prompt (AGENTS.md, skills, tools) in tokens.

Replace it with a single standalone LLM call inside the pi process,
using completeSimple from pi-ai (the same primitive compaction's
completeSummarization wraps) with a compact transcript and a short
system prompt. Resolve the model via the session model registry
(PI_CMUX_AUTOTITLE_MODEL or ctx.model) and auth via
modelRegistry.getApiKeyAndHeaders.

The old child-process guards (env sanitization, stdin EOF, recursion
gate) are gone with the child; the TUI mode gate stays. The naming pass
now aborts when the user starts a new turn.

Also bump devDependency pi-coding-agent to 0.83.0 (+ pi-ai) to match
the running pi and pass the repo typecheck.
Important content in a message tends to sit at the start (the request,
the error) and the end (the outcome, the question); the middle is
usually detail. So instead of a hard head-only cut at 1500 chars,
compress each cached message to ~300 chars with a 60/40 head/tail
split around an ellipsis.
"pi-cmux": { "autotitle": true } in ~/.pi/agent/settings.json or
project-local .pi/settings.json (project wins), the same settings
pattern pi-cmux.commands uses. The env var still works and either
source enables the feature. Bad files/sections are ignored, never
fatal.
Users don't discover opt-in features; on-by-default with an escape
hatch gives the feature a chance to be seen. Flip the default:

- No env, no settings: enabled.
- PI_CMUX_AUTOTITLE_DISABLED=1 opts out (env).
- "pi-cmux": { "autotitle": false } opts out (settings, project wins).
- PI_CMUX_AUTOTITLE=1 still force-enables (kept for compatibility).
@chenzier
chenzier force-pushed the feat/conversation-tab-titles branch from 1be452a to 562642d Compare September 9, 2026 08:27
@chenzier

chenzier commented Sep 9, 2026

Copy link
Copy Markdown
Author

Rebased onto the latest main (resolved conflicts with the 0.1.18–0.1.20 changes) — the PR is mergeable again, and all 86 tests plus tsc --strict typecheck pass against Pi 0.85.1.

One design point I'd like your input on before you dig in: the feature is on by default, with opt-out via PI_CMUX_AUTOTITLE_DISABLED=1 or Pi settings. My reasoning was that opt-in features stay undiscovered, and the failure budget guarantees any summarizer failure is silent and non-blocking. Happy to flip it to opt-in if you'd rather keep the default behavior untouched.

Reviewing tip: the core logic is contained in extensions/cmux-autotitle.ts (~200 lines); the rest of the diff is README/CHANGELOG docs, config plumbing, and the lockfile bump.

Merge current main. Generate titles off the settlement path through Pi's model registry, honor project overrides and manual names, cancel stale work, verify cmux before requesting a title, and restore persisted titles across session changes. Add regression coverage and remove the unnecessary Pi AI dependency.
@javiermolinar javiermolinar changed the title feat: conversation-driven tab titles (in-process LLM, on by default) feat: opt-in conversation tab titles Sep 28, 2026
@javiermolinar

Copy link
Copy Markdown
Owner

Pushed maintainer fixes in 611a55f and merged current main into this branch without rewriting contributor history. These changes supersede the original implementation details above; automatic titles are now opt-in.

  • Use a background task after idle settlement instead of awaiting an LLM inside agent_end.
  • Respect project-over-global settings and explicit opt-out; verify an interactive cmux caller before sending conversation excerpts.
  • Use the session model registry, including custom providers/auth, without a separate Pi AI dependency.
  • Cancel and discard stale work on new turns, session changes, shutdown, timeout, or /name; serialize renames so manual names win.
  • Persist successful titles and restore destination session/branch state across reload, resume, fork, and tree navigation.
  • Add lifecycle/configuration/failure regression tests and document provider usage costs.

Validated with pinned dependencies on Node 22.19.0: typecheck, npm test (431 passed, six optional installed-CLI tests skipped), and pack:check. Provider and cmux behavior was tested with stubs, not paid live requests. This PR has not been merged into main.

@javiermolinar

Copy link
Copy Markdown
Owner

Thank you!

@javiermolinar
javiermolinar merged commit 4bcf391 into javiermolinar:main Sep 28, 2026
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.

2 participants