refactor(tui): use session name for terminal tab identity

This commit is contained in:
CodeWhale Bot committed 2026-08-15 23:09:47 -07:00
1 parent cf107f8800
commit 24c7dee466
34 files changed
+193 -428

No files matched your search

+10 -6
View File
@@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `/title [name|off]` sets a per-session tab/window title, shown as
`[title] …` in front of the terminal window title (`Codewhale` /
`reasoning…` / `using tool…` / `done`). The `title` config key supplies
the default (`/config title … --save` persists it); multi-window
workflows can tell parallel sessions apart at a glance. `/title` is
independent of `/rename`, which keeps naming the session in the picker.
- Terminal tab/window titles now carry the existing saved session name before
the live state (`Codewhale`, `reasoning…`, `using tool…`, `done`), so parallel
sessions are identifiable at a glance without a second title setting.
`/title <name>` is a discoverable alias for `/rename`; both update the one
session name shown in the picker, composer, and terminal tab. Control and
bidi-format characters are stripped before any name reaches OSC 0.
- Children (sub-agents and Fleet workers) inherit the session's permission
posture faithfully: Auto-Review's deterministic floor and model guardian
decide a worker's held calls (fail closed when unavailable, never a
@@ -97,8 +97,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
reduced-motion fallbacks); DSH exposes no custom-theme API, so the sheet is
labeled an unsupported overlay and is never injected. See
`docs/INTEGRATIONS_DSH.md`.
### Fixed
- Session titles truncate by character count, not byte offset, so
multi-byte titles (CJK, emoji) cut at the intended width and word
boundary instead of past the limit (#5415).
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter is
gone so expanding a pane rematerializes layout the same way shrinking does.
-8
View File
@@ -258,14 +258,6 @@ approval_policy = "on-request" # on-request | untrusted | never
sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access | external-sandbox
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
# Optional tab/window title shown as `[title] …` in front of the terminal
# window title (the `Codewhale` / `reasoning…` / `using tool…` / `done`
# states). Multi-window setups can give each workspace its own `--config`
# file (or profile) so alt-tabbed sessions are identifiable at a glance.
# The `/title` command overrides this per session; `/config title … --save`
# persists a new default here. Run `/title off` to drop a session override.
# title = "workspace-x"
# Typed permission rules live in a sibling `permissions.toml` file, not in
# config.toml. Each `[[rules]]` entry accepts `tool`, optional `command`
# or `path`, optional absolute `workspace`, optional `command_exact = true`,
+117 -7
View File
@@ -9,12 +9,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `/title [name|off]` sets a per-session tab/window title, shown as
`[title] …` in front of the terminal window title (`Codewhale` /
`reasoning…` / `using tool…` / `done`). The `title` config key supplies
the default (`/config title … --save` persists it); multi-window
workflows can tell parallel sessions apart at a glance. `/title` is
independent of `/rename`, which keeps naming the session in the picker.
- Terminal tab/window titles now carry the existing saved session name before
the live state (`Codewhale`, `reasoning…`, `using tool…`, `done`), so parallel
sessions are identifiable at a glance without a second title setting.
`/title <name>` is a discoverable alias for `/rename`; both update the one
session name shown in the picker, composer, and terminal tab. Control and
bidi-format characters are stripped before any name reaches OSC 0.
- Children (sub-agents and Fleet workers) inherit the session's permission
posture faithfully: Auto-Review's deterministic floor and model guardian
decide a worker's held calls (fail closed when unavailable, never a
prompt); under Ask a held call is raised in the parent's approval UI and
the worker waits visibly; Full Access still fails closed on the safety
floor. Each prompt-less decision is a one-line note in that worker's
transcript (focus mode) and an audit-log record.
- Worker role defaults keep what the role does not intend to withhold:
every built-in role keeps network reads; `planner` may run read-only
shell probes; `custom` inherits the parent's write/network/shell posture
and is narrowed only by its explicit tool list or the spawning call.
Read-only roles (`scout`, `reviewer`, `planner`, `verifier`,
`consultant`) still never write the workspace. The focused worker's
header states its effective posture from the runtime snapshot.
- `/workflow status`, `/workflow cancel [run_id]`, `/workflow settings`, and
`/workflow help` are answered by Codewhale itself from the run journal and
live run state — no model turn — and `/workflow run <path>` launches a
checked-in workflow as-is. `/config workflow` and `/config goal` explain
the effective tables. The workflow tool now honors the session `[workflow]`
table (`automatic`, `auto_start_read_only`, `require_approval_for_writes`,
limits) instead of product defaults.
- Goal mode enters as readily as DeepSeek Harness: the agent may create the
session goal when a direct request describes a verifiable multi-turn end
state, and Codewhale shows a one-line `Goal set` receipt with how to pause
or clear it. Bare `/goal` shows plain progress (and how to continue when no
turn is running), prints usage on an empty session instead of asking the
model, and `/goal help|status` are reserved words.
- Whale Teams in the terminal: the six Signal Cut whale identities (Scout,
Patch, Harbor, Echo, Keel, Lantern) appear as species badges on `/fleet`
roster rows and worker rows, with an identity portrait in the roster detail
pane and a six-state word (Resting, Thinking, Working, Waiting for you,
Blocked, Offline) derived only from the child's real runtime status. Colors
come from the theme tokens, every glyph has an ASCII fallback, and the
working wake animates only under full motion. See
`docs/design/WHALE_TEAMS_TUI.md`.
- A session metrics strip on the phase row (`4 turns · 108 steps │ LLM
11m46s · Tool call 1m52s │ TTFT avg 1.5s · 120 tok/s │ Cache hit 99% │
Input 9.3M`), on by default as the `session_metrics` footer item
(`/statusline`, `[tui].status_items`). Every value comes from engine
receipts — turn starts, per-model-call usage with stream time,
time-to-first-token and whole-call time, tool start/complete edges, and
provider-reported cache and input tokens. Cells without evidence are
omitted, never estimated. `/status` prints the untrimmed line; the phase
row sheds its lowest-value groups to fit the columns it actually has.
- Auto-Review decisions nobody was prompted for are now visible in the
transcript as one-line notes: model-guardian allow/deny verdicts with
their risk tier and stated reason, guardian failures (denied, fail
closed), deterministic policy blocks, and holds Auto-Review denied
without pausing. The audit log keeps the full record. `/permissions`
ends with the active posture, what it decides on its own versus never,
and the audit-log path. The footer's `Esc to interrupt` hint is
localized. See `docs/design/AUTO_MODE_PARITY.md` for the Claude Code /
Kimi Code parity ledger and follow-ups.
- `codewhale integrations dsh status|plan|connect|update|launch|disable|enable|remove`
connects an existing official DeepSeek Harness (`dsh` 0.1.0-rc.6, verified)
through Codewhale using only its documented seams: a `--patch` overlay that
pins the exact Codewhale provider/model/endpoint identity (native
`deepseek-official` route, or a hand-declared `openai-completions` route
named `codewhale-<provider>` for OpenAI-compatible providers), the
Codewhale permission posture exported as `DSH_PERMISSION_MODE`, and an
append-only receipt. Codewhale writes only under
`$CODEWHALE_HOME/integrations/dsh/`, never copies API keys or edits DSH
files, never broadens permissions (`--allow-full-access` only mirrors an
existing Codewhale full-access posture), and reports not-installed /
offline / incompatible / detected / connected / stale-config /
stale-version / disabled honestly. Anthropic Messages and OpenAI Responses
routes are refused as not carriable. The documented DSH plugin path is an
explicit opt-in: `install-bundle` materializes a Codewhale bundle package
(`codewhale-dsh-bundle`, MIT notice retained) and installs it with
`dsh plugin --profile codewhale add <path>` into a dedicated `codewhale`
profile (pnpm required, reported truthfully when missing; `web`/`headless`
untouched), so `dsh --profile codewhale` alone carries the identity;
`update` regenerates the bundle patch and `remove-bundle` reverses it,
leaving the DSH-owned profile directory in place. `/setup tools` and `codewhale doctor`
show the read-only detection state; `doctor` also lists the DSH read-only
credential consent alongside Codex and Grok. The optional `--skin` export
writes a Codewhale token stylesheet generated from the TUI palette
(Blue Stage dark/light, ombre water column, mode/permission/state colors,
reduced-motion fallbacks); DSH exposes no custom-theme API, so the sheet is
labeled an unsupported overlay and is never injected. See
`docs/INTEGRATIONS_DSH.md`.
### Fixed
@@ -33,6 +115,19 @@ a published OpenAI-compatible host ship here (#5350).
### Changed
- Prompt-cache prefix is pinned for the session. The tool loop no longer
recomposes the system prompt from disk on every model step, so an agent
writing a file no longer busts the provider KV prefix cache mid-turn. The
system prompt and tool catalog are re-composed only on a declared header
change (`/model`, mode, goal, session resume), which re-pins under a logged
reason; an undeclared change is reported as drift and the original pin is
kept instead of silently becoming the new baseline. Workspace, AGENTS.md,
skills, memory, and goal drift now reaches the model as one bounded
`<context_update>` user message at the next user turn — a history append,
not a header rewrite. `/cache stats` shows the pin reason, the last-miss
reason, the undeclared-drift count, and the context-update count. See
[docs/CACHE.md](docs/CACHE.md).
- Plugin compatibility is now per-component. A reviewed, trusted, enabled
bundle that mixes Skills or MCP with unsupported commands, agents, hooks,
LSP, native, filesystem-roots, or lifecycle-mutation declarations keeps the
@@ -68,7 +163,10 @@ a published OpenAI-compatible host ship here (#5350).
of sliding under the rail.
- Z.ai `GLM-5.3` is live on the Coding Plan and is now the default direct
Z.ai model. Explicit `GLM-5.2` selections keep their own id.
Z.ai model: `DEFAULT_ZAI_MODEL` resolves to `GLM-5.3` in both
`codewhale-tui` and `codewhale-config`, and it is the first `/model` row
after `/provider zai`. Explicit `GLM-5.2` selections (`model = "GLM-5.2"`
and its `glm-5.2` aliases) keep their own id — only the default moved.
Limits and reasoning options still inherit from `GLM-5.2` until Z.ai
publishes distinct 5.3 numbers. No USD price is claimed. A live call
can still 429 with entitlement code 1311 on accounts that are not
@@ -184,6 +282,14 @@ a published OpenAI-compatible host ship here (#5350).
(44 runnable routes). Antigravity stays credential-plane-only.
Harvested from #5398 (Lstarsky0) with that correction.
- The website models page carries a truthful read-only settings preview
built from repository facts; it never implies the site can change local
configuration (#5370, #5411, mvanhorn).
- The canonical `ultra` reasoning effort now maps to each provider's
maximum tier alongside the legacy `ultracode` alias, instead of being
silently dropped (#5303, #5409, buiducnhat).
### Removed
- The source-structure budget ratchet (CI step, checker, baseline JSON).
@@ -220,6 +326,10 @@ a published OpenAI-compatible host ship here (#5350).
(#5383/#5384); macOS agy fixture canonicalization (#5392); zh-Hans 宪章
terminology (#5397); regenerated website facts harvested and corrected
from #5398.
- Matt Van Horn (@mvanhorn) — read-only models settings preview on the
website (#5411, fixes #5370).
- Nhat Bui (@buiducnhat) — canonical `ultra` reasoning effort mapped across
provider effort tables (#5409).
## [0.9.7] - 2026-08-12
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale no carrega, migra ni sincronitza codi font local amb el Work allotjat. Utilitza {command} per començar des de la punta de la branca disponible a GitHub o CNB. Els commits no enviats, els fitxers modificats o ignorats, els secrets i l'estat de la sessió es mantenen en local.",
"CmdRemoteEnvBrowserLabel": "Work allotjat de Codewhale",
"CmdRenameDescription": "Reanomena la sessió actual",
"CmdTitleDescription": "Defineix un títol de pestanya/finestra mostrat com a [title] … al títol del terminal",
"CmdTitleDescription": "Defineix el nom de la sessió i el títol de la pestanya/finestra del terminal",
"CmdRestoreDescription": "Reverteix l'espai de treball a una instantània anterior pre/post-torn. Sense argument, llista les instantànies recents.",
"CmdRetryDescription": "Reintenta l'última petició",
"CmdReviewDescription": "Executa una revisió de codi estructurada sobre un fitxer, diff o PR",
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale lädt lokalen Quellcode nicht in gehostetes Work hoch und migriert oder synchronisiert ihn nicht dorthin. Verwende {command}, um von der bei GitHub oder CNB verfügbaren Branch-Spitze zu starten. Nicht gepushte Commits, geänderte oder ignorierte Dateien, Geheimnisse und Sitzungsstatus bleiben lokal.",
"CmdRemoteEnvBrowserLabel": "Gehostetes Work von Codewhale",
"CmdRenameDescription": "Aktuelle Sitzung umbenennen",
"CmdTitleDescription": "Registerkarten-/Fenstertitel festlegen, der als [title] … im Terminaltitel erscheint",
"CmdTitleDescription": "Sitzungsnamen und Titel des Terminal-Tabs/Fensters festlegen",
"CmdRestoreDescription": "Workspace auf einen früheren Pre-/Post-Turn-Snapshot zurücksetzen. Ohne Argument werden die letzten Snapshots aufgelistet.",
"CmdRetryDescription": "Letzte Anfrage wiederholen",
"CmdReviewDescription": "Strukturiertes Code-Review für eine Datei, einen Diff oder PR ausführen",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale does not upload, migrate, or sync local source into hosted Work. Use {command} to start from the branch tip available at GitHub or CNB. Unpushed commits, dirty or ignored files, secrets, and session state stay local.",
"CmdRemoteEnvBrowserLabel": "Codewhale hosted Work",
"CmdRenameDescription": "Rename the current session",
"CmdTitleDescription": "Set a tab/window title shown as [title] … in the terminal title",
"CmdTitleDescription": "Name the current session and its terminal tab/window",
"CmdRestoreDescription": "Roll back the workspace to a prior pre/post-turn snapshot. With no arg, lists recent snapshots.",
"CmdRetryDescription": "Retry the last request",
"CmdReviewDescription": "Run a structured code review on a file, diff, or PR",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale no carga, migra ni sincroniza el código fuente local con el Work alojado. Usa {command} para iniciar desde la punta de la rama disponible en GitHub o CNB. Los commits sin enviar, los archivos modificados o ignorados, los secretos y el estado de la sesión permanecen locales.",
"CmdRemoteEnvBrowserLabel": "Work alojado de Codewhale",
"CmdRenameDescription": "Renombrar la sesión actual",
"CmdTitleDescription": "Establecer un título de pestaña/ventana mostrado como [title] … en el título del terminal",
"CmdTitleDescription": "Nombrar la sesión actual y su pestaña/ventana de terminal",
"CmdRestoreDescription": "Revertir el workspace a un snapshot pre/post-turno anterior. Sin argumento, lista los snapshots recientes.",
"CmdRetryDescription": "Repetir la última solicitud",
"CmdReviewDescription": "Ejecutar una revisión de código estructurada en un archivo, diff o PR",
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale ne téléverse, ne migre et ne synchronise pas le code source local dans le Work hébergé. Utilisez {command} pour démarrer depuis la pointe de la branche disponible sur GitHub ou CNB. Les commits non poussés, les fichiers modifiés ou ignorés, les secrets et l'état de la session restent locaux.",
"CmdRemoteEnvBrowserLabel": "Work hébergé de Codewhale",
"CmdRenameDescription": "Renommer la session en cours",
"CmdTitleDescription": "Définir un titre d'onglet/fenêtre affiché comme [title] … dans le titre du terminal",
"CmdTitleDescription": "Nommer la session et son onglet ou sa fenêtre de terminal",
"CmdRestoreDescription": "Restaurer le workspace à un snapshot pré/post-tour antérieur. Sans argument, liste les snapshots récents.",
"CmdRetryDescription": "Réessayer la dernière requête",
"CmdReviewDescription": "Lancer une revue de code structurée sur un fichier, un diff ou une PR",
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale लोकल सोर्स को होस्टेड Work में अपलोड, माइग्रेट या सिंक नहीं करता। GitHub या CNB पर उपलब्ध ब्रांच टिप से शुरू करने के लिए {command} का उपयोग करें। पुश न किए गए कमिट, बदली हुई या इग्नोर की गई फ़ाइलें, सीक्रेट और सत्र स्थिति लोकल ही रहती हैं।",
"CmdRemoteEnvBrowserLabel": "Codewhale होस्टेड Work",
"CmdRenameDescription": "वर्तमान सत्र का नाम बदलें",
"CmdTitleDescription": "टर्मिनल शीर्षक में [title] … के रूप में दिखने वाला टैब/विंडो शीर्षक सेट करें",
"CmdTitleDescription": "वर्तमान सत्र और उसके टर्मिनल टैब/विंडो का नाम सेट करें",
"CmdRestoreDescription": "वर्कस्पेस को पूर्व pre/post-turn स्नैपशॉट पर वापस लाएँ। बिना आर्ग के हालिया स्नैपशॉट सूचीबद्ध करता है।",
"CmdRetryDescription": "अंतिम अनुरोध पुनः प्रयास करें",
"CmdReviewDescription": "फ़ाइल, diff या PR पर संरचित कोड समीक्षा चलाएँ",
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale tidak mengunggah, memigrasikan, atau menyinkronkan kode sumber lokal ke Work terhosting. Gunakan {command} untuk memulai dari ujung branch yang tersedia di GitHub atau CNB. Commit yang belum di-push, file yang berubah atau diabaikan, rahasia, dan status sesi tetap lokal.",
"CmdRemoteEnvBrowserLabel": "Work terhosting Codewhale",
"CmdRenameDescription": "Ubah nama sesi saat ini",
"CmdTitleDescription": "Atur judul tab/jendela yang ditampilkan sebagai [title] … pada judul terminal",
"CmdTitleDescription": "Beri nama sesi saat ini dan tab/jendela terminalnya",
"CmdRestoreDescription": "Kembalikan workspace ke snapshot pra/pasca-giliran sebelumnya. Tanpa argumen, menampilkan snapshot terbaru.",
"CmdRetryDescription": "Coba lagi permintaan terakhir",
"CmdReviewDescription": "Jalankan tinjauan kode terstruktur pada file, diff, atau PR",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale はローカルソースをホスト型 Work にアップロード、移行、同期しません。{command} を使用して、GitHub または CNB で利用可能なブランチ先端から開始してください。未プッシュのコミット、変更済みまたは無視対象のファイル、シークレット、セッション状態はローカルに残ります。",
"CmdRemoteEnvBrowserLabel": "Codewhale ホスト型 Work",
"CmdRenameDescription": "現在のセッションの名前を変更",
"CmdTitleDescription": "タブ/ウィンドウのタイトルを設定(端末タイトルに [title] … と表示)",
"CmdTitleDescription": "現在のセッションと端末タブ/ウィンドウに名前を付ける",
"CmdRestoreDescription": "ワークスペースを以前のターン前/後スナップショットへロールバック。引数なしで最近のスナップショットを一覧表示。",
"CmdRetryDescription": "直前のリクエストを再試行",
"CmdReviewDescription": "ファイル・diff・PR に対して構造化コードレビューを実行",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale은 로컬 소스를 호스팅 Work에 업로드, 마이그레이션 또는 동기화하지 않습니다. {command}을 사용하여 GitHub 또는 CNB에서 사용할 수 있는 브랜치 끝에서 시작하세요. 푸시하지 않은 커밋, 변경되었거나 무시된 파일, 비밀, 세션 상태는 로컬에 남습니다.",
"CmdRemoteEnvBrowserLabel": "Codewhale 호스팅 Work",
"CmdRenameDescription": "현재 세션 이름을 바꿉니다",
"CmdTitleDescription": "터미널 제목에 [title] … 로 표시되는 탭/창 제목을 설정",
"CmdTitleDescription": "현재 세션과 터미널 탭/창의 이름을 설정",
"CmdRestoreDescription": "작업 공간을 이전 턴 전/후 스냅샷으로 되돌립니다. 인자가 없으면 최근 스냅샷 목록을 표시합니다.",
"CmdRetryDescription": "마지막 요청을 재시도합니다",
"CmdReviewDescription": "파일, diff, PR에 대해 구조화된 코드 리뷰를 실행합니다",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "O Codewhale não envia, migra nem sincroniza o código-fonte local com o Work hospedado. Use {command} para iniciar a partir da ponta da branch disponível no GitHub ou CNB. Commits não enviados, arquivos modificados ou ignorados, segredos e o estado da sessão permanecem locais.",
"CmdRemoteEnvBrowserLabel": "Work hospedado do Codewhale",
"CmdRenameDescription": "Renomear a sessão atual",
"CmdTitleDescription": "Definir um título de aba/janela mostrado como [title] … no título do terminal",
"CmdTitleDescription": "Nomear a sessão atual e sua aba/janela do terminal",
"CmdRestoreDescription": "Reverter o workspace a um snapshot pré/pós-turno anterior. Sem argumento, lista os snapshots recentes.",
"CmdRetryDescription": "Repetir a última requisição",
"CmdReviewDescription": "Executar uma revisão de código estruturada em um arquivo, diff ou PR",
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale не загружает, не переносит и не синхронизирует локальный исходный код с размещённым Work. Используйте {command}, чтобы начать с вершины ветки, доступной на GitHub или CNB. Неотправленные коммиты, изменённые или игнорируемые файлы, секреты и состояние сеанса остаются локальными.",
"CmdRemoteEnvBrowserLabel": "Размещённый Work Codewhale",
"CmdRenameDescription": "Переименовать текущую сессию",
"CmdTitleDescription": "Установить заголовок вкладки/окна, отображаемый как [title] … в заголовке терминала",
"CmdTitleDescription": "Задать имя текущей сессии и вкладки/окна терминала",
"CmdRestoreDescription": "Откатить рабочую область к прежнему снимку до/после хода. Без аргумента — список недавних снимков.",
"CmdRetryDescription": "Повторить последний запрос",
"CmdReviewDescription": "Запустить структурированное ревью кода для файла, diff или PR",
+1 -1
View File
@@ -345,7 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale не завантажує, не переносить і не синхронізує локальний вихідний код із розміщеним Work. Скористайтеся {command}, щоб почати з вершини гілки, доступної на GitHub або CNB. Ненадіслані коміти, змінені чи ігноровані файли, секрети й стан сеансу залишаються локальними.",
"CmdRemoteEnvBrowserLabel": "Розміщений Work Codewhale",
"CmdRenameDescription": "Перейменувати поточний сеанс",
"CmdTitleDescription": "Встановити заголовок вкладки/вікна, який відображається як [title] … у заголовку термінала",
"CmdTitleDescription": "Назвати поточний сеанс і вкладку/вікно термінала",
"CmdRestoreDescription": "Відкотити робочу область до попереднього знімка до/після кроку. Без аргумента — показує останні знімки.",
"CmdRetryDescription": "Повторити останній запит",
"CmdReviewDescription": "Запустити структурований огляд коду для файлу, diff або PR",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale không tải lên, di chuyển hoặc đồng bộ mã nguồn cục bộ vào Work được lưu trữ. Dùng {command} để bắt đầu từ đầu nhánh hiện có trên GitHub hoặc CNB. Các commit chưa đẩy, tệp đã thay đổi hoặc bị bỏ qua, bí mật và trạng thái phiên vẫn ở cục bộ.",
"CmdRemoteEnvBrowserLabel": "Work được Codewhale lưu trữ",
"CmdRenameDescription": "Đổi tên phiên làm việc hiện tại",
"CmdTitleDescription": "Đặt tiêu đề tab/cửa sổ hiển thị dạng [title] … trong tiêu đề terminal",
"CmdTitleDescription": "Đặt tên phiên hiện tại và tab/cửa sổ terminal",
"CmdRestoreDescription": "Khôi phục không gian làm việc về bản chụp trước/sau lượt. Nếu không có đối số, hiển thị các bản chụp gần đây.",
"CmdRetryDescription": "Thử lại yêu cầu gần nhất",
"CmdReviewDescription": "Chạy một quy trình xem xét mã nguồn có cấu trúc trên tệp, diff hoặc PR",
+1 -1
View File
@@ -348,7 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale 不会将本地源代码上传、迁移或同步到托管 Work。使用 {command} 从 GitHub 或 CNB 上可用的分支尖端开始。未推送的提交、已修改或已忽略的文件、密钥和会话状态仍保留在本地。",
"CmdRemoteEnvBrowserLabel": "Codewhale 托管 Work",
"CmdRenameDescription": "重命名当前会话",
"CmdTitleDescription": "设置标签页标题,终端标题显示为 [title] …",
"CmdTitleDescription": "命名当前会话及其终端标签页/窗口",
"CmdRestoreDescription": "将工作区回滚到此前的轮次前/后快照。不带参数时列出最近的快照。",
"CmdRetryDescription": "重试上一次请求",
"CmdReviewDescription": "对文件、diff 或 PR 进行结构化代码审查",
+1 -1
View File
@@ -295,7 +295,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale 不會將本機原始碼上傳、移轉或同步到託管 Work。使用 {command} 從 GitHub 或 CNB 上可用的分支尖端開始。尚未推送的提交、已修改或已忽略的檔案、密鑰和工作階段狀態仍保留在本機。",
"CmdRemoteEnvBrowserLabel": "Codewhale 託管 Work",
"CmdRenameDescription": "重新命名目前工作階段",
"CmdTitleDescription": "設定標籤頁/視窗標題,終端機標題顯示為 [title] …",
"CmdTitleDescription": "命名目前工作階段及其終端機標籤頁/視窗",
"CmdRestoreDescription": "將工作區回復到此前的輪次前/後快照。不帶參數時列出最近的快照。",
"CmdRetryDescription": "重試上一次請求",
"CmdReviewDescription": "對檔案、diff 或 PR 進行結構化程式碼審查",
@@ -281,11 +281,6 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
};
Some(config.deepseek_base_url())
}
"title" | "window_title" | "tab_title" => Some(
app.window_title_prefix()
.map(str::to_string)
.unwrap_or_else(|| "(unset)".to_string()),
),
"provider_url" | "provider_base_url" | "endpoint" => {
let config = match Config::load(app.config_path.clone(), app.config_profile.as_deref())
{
@@ -1763,30 +1758,6 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
"base_url must be saved with --save; client base URL is loaded from config on startup. Restart and re-open your session after saving.",
);
}
"title" | "window_title" | "tab_title" => {
let value = value.trim();
if value.is_empty() {
return CommandResult::error(
"title cannot be empty; use /title off to clear a session title",
);
}
if value.chars().count() > 100 {
return CommandResult::error("Title too long (max 100 characters)");
}
let suffix = if persist {
match persist_root_string_key(app.config_path.as_deref(), "title", value) {
Ok(path) => format!(" (saved to {})", path.display()),
Err(err) => return CommandResult::error(format!("Failed to save: {err}")),
}
} else {
" (session only, add --save to persist)".to_string()
};
app.title_default = Some(value.to_string());
app.needs_redraw = true;
return CommandResult::message(format!(
"title = {value}{suffix} — terminal window titles now read [\"{value}\"] … until /title overrides this session"
));
}
"provider_url" | "provider_base_url" | "endpoint" => {
let value = match resolve_provider_url_value(app.api_provider, value) {
Ok(value) => value,
@@ -161,7 +161,6 @@ pub fn clear(app: &mut App) -> CommandResult {
app.current_session_id = None;
app.current_session_metadata = None;
app.session_title = None;
app.window_title = None;
let locale = app.ui_locale;
let message = tr(locale, MessageId::ClearConversation).to_string();
CommandResult::with_message_and_action(
@@ -108,7 +108,9 @@ pub(crate) fn rename_with_manager(
"Session renamed, but Work views were not published: {err}"
));
}
CommandResult::message(format!("Session renamed to \"{new_title}\""))
CommandResult::message(format!(
"Session and terminal tab renamed to \"{new_title}\""
))
}
Err(e) => CommandResult::error(format!("Could not save session: {e}")),
}
@@ -153,8 +153,6 @@ pub fn fork_from_session(app: &mut App, session_id_or_prefix: &str) -> CommandRe
app.current_session_id = Some(forked.metadata.id.clone());
app.current_session_metadata = Some(forked.metadata.clone());
app.session_title = Some(forked.metadata.title.clone());
// A fork starts as its own session: no inherited tab/window title.
app.window_title = None;
let parent_label = crate::session_manager::truncate_id(&source_session.metadata.id).to_string();
let fork_label = crate::session_manager::truncate_id(&forked.metadata.id).to_string();
CommandResult::with_message_and_action(
@@ -275,7 +273,6 @@ pub fn fork(app: &mut App) -> CommandResult {
app.current_session_id = Some(forked.metadata.id.clone());
app.current_session_metadata = Some(forked.metadata.clone());
app.session_title = Some(forked.metadata.title.clone());
app.window_title = None;
let fork_id = forked.metadata.id.clone();
let parent_label = crate::session_manager::truncate_id(&parent.metadata.id).to_string();
let fork_label = crate::session_manager::truncate_id(&fork_id).to_string();
@@ -334,7 +331,6 @@ pub fn new_session(app: &mut App, arg: Option<&str>) -> CommandResult {
app.current_session_id = Some(new_id.clone());
app.current_session_metadata = None;
app.session_title = Some(crate::session_manager::DEFAULT_SESSION_TITLE.to_string());
app.window_title = None;
app.scroll_to_bottom();
CommandResult::with_message_and_action(
+5 -251
View File
@@ -1,25 +1,15 @@
//! `/title` command — set a distinct tab/window title for the current
//! session, shown as `[title] …` in front of the terminal window title.
//!
//! This is deliberately separate from `/rename`, which changes the session
//! *name* shown in the composer border and session picker. `/title` only
//! affects the terminal window/tab title, so parallel sessions — each in
//! its own terminal window — are identifiable at a glance while they are
//! reasoning, using tools, or waiting.
//! Discoverable terminal-tab name command backed by the existing session name.
use crate::commands::traits::{CommandInfo, RegisterCommand};
use crate::localization::MessageId;
use crate::session_manager::{SessionManager, update_session};
use crate::tui::app::App;
use super::CommandResult;
const MAX_TITLE_LEN: usize = 100;
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "title",
aliases: &["tabtitle", "window-title"],
usage: "/title [new title|off]",
usage: "/title <name>",
description_id: MessageId::CmdTitleDescription,
};
@@ -31,245 +21,9 @@ impl RegisterCommand for TitleCmd {
}
fn execute(app: &mut App, arg: Option<&str>) -> CommandResult {
set_window_title(app, arg)
}
}
/// Set (or clear) the current session's tab/window title.
///
/// - `/title <new title>` — set the window-title prefix for this session and
/// persist it on the saved session so it survives restarts.
/// - `/title off` — clear the session-level title; the `title` config
/// default (if any) still applies.
/// - `/title` with no argument — report the current effective title.
pub fn set_window_title(app: &mut App, arg: Option<&str>) -> CommandResult {
let arg = arg.map(str::trim).filter(|s| !s.is_empty());
let Some(arg) = arg else {
let current = app.window_title_prefix().unwrap_or("unset");
let source = if app.window_title.is_some() {
" (session)"
} else if app.title_default.is_some() {
" (config default)"
} else {
""
let Some(title) = arg.map(str::trim).filter(|title| !title.is_empty()) else {
return CommandResult::error("Usage: /title <name>");
};
return CommandResult::message(format!("Window title: [{current}]{source}"));
};
if arg == "off" || arg == "clear" || arg == "none" {
let manager = match resolve_manager() {
Ok(manager) => manager,
Err(error) => return error,
};
return set_window_title_with_manager(app, None, &manager);
}
if arg.chars().count() > MAX_TITLE_LEN {
return CommandResult::error(format!("Title too long (max {MAX_TITLE_LEN} characters)"));
}
let manager = match resolve_manager() {
Ok(manager) => manager,
Err(error) => return error,
};
set_window_title_with_manager(app, Some(arg.to_string()), &manager)
}
#[allow(clippy::result_large_err)]
fn resolve_manager() -> Result<SessionManager, CommandResult> {
SessionManager::default_location()
.map_err(|e| CommandResult::error(format!("Could not open sessions directory: {e}")))
}
/// Set or clear the session-level window title with an explicit manager.
///
/// Mirrors the `/rename` write path (snapshot Work state and live metadata
/// before saving so the write cannot revert unsaved messages), then updates
/// `app.window_title` in the same step the disk write lands.
pub(crate) fn set_window_title_with_manager(
app: &mut App,
title: Option<String>,
manager: &SessionManager,
) -> CommandResult {
let session_id = match &app.current_session_id {
Some(id) => id.clone(),
None => {
return CommandResult::error(
"No active session. Send a message first to start a session.",
);
}
};
let mut session = match manager.load_session(&session_id) {
Ok(s) => s,
Err(e) => return CommandResult::error(format!("Could not load session: {e}")),
};
// Sync with current App state to avoid overwriting unsaved messages.
session = update_session(
session,
&app.api_messages,
u64::from(app.session.total_tokens),
app.system_prompt.as_ref(),
);
session.work_state = match app.work_state_snapshot() {
Ok(state) => state,
Err(err) => {
return CommandResult::error(format!(
"Could not snapshot Work state before setting title: {err}"
));
}
};
session.context_references = app.session_context_references.clone();
session.artifacts = app.session_artifacts.clone();
session.last_auto_route = app.auto_route_for_persistence();
session
.metadata
.set_model_provider_route(app.api_provider.as_str(), app.provider_id_for_persistence());
session.metadata.workspace.clone_from(&app.workspace);
session.metadata.mode = Some(app.mode.as_setting().to_string());
app.sync_cost_to_metadata(&mut session.metadata);
session.window_title = title.clone();
match manager.save_session(&session) {
Ok(_) => {
app.window_title = title.clone();
// The render loop syncs the resolved prefix into the terminal
// title; force a frame so the change lands immediately.
app.needs_redraw = true;
if let Err(err) = app.publish_pending_work_state() {
return CommandResult::error(format!(
"Window title saved, but Work views were not published: {err}"
));
}
match title {
Some(title) => CommandResult::message(format!(
"Window title set to \"{title}\" — the terminal tab now reads [\"{title}\"] …"
)),
None => CommandResult::message(
"Window title cleared (the config default still applies if set)",
),
}
}
Err(e) => CommandResult::error(format!("Could not save session: {e}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::session_manager::{SessionManager, create_saved_session_with_mode};
use crate::tui::app::{App, TuiOptions};
use tempfile::TempDir;
fn make_app(tmpdir: &TempDir) -> App {
App::new(
TuiOptions {
skills_dir: tmpdir.path().join("skills"),
memory_path: tmpdir.path().join("memory.md"),
notes_path: tmpdir.path().join("notes.txt"),
mcp_config_path: tmpdir.path().join("mcp.json"),
..crate::test_support::test_tui_options(tmpdir.path())
},
&Config::default(),
)
}
fn make_session_manager(tmpdir: &TempDir) -> SessionManager {
SessionManager::new(tmpdir.path().join("sessions")).unwrap()
}
#[test]
fn no_active_session_reports_usage_error() {
let tmp = TempDir::new().unwrap();
let mut app = make_app(&tmp);
app.current_session_id = None;
let result = set_window_title(&mut app, Some("task-7"));
assert!(result.is_error);
assert!(result.message.unwrap().contains("No active session"));
}
#[test]
fn set_title_persists_on_the_saved_session() {
let tmp = TempDir::new().unwrap();
let manager = make_session_manager(&tmp);
let mut app = make_app(&tmp);
let mut session = create_saved_session_with_mode(
&[],
"deepseek-v4-pro",
tmp.path(),
0,
None,
Some("agent"),
);
session.metadata.id = "title-test".to_string();
session.metadata.title = "Original Name".to_string();
manager.save_session(&session).unwrap();
app.current_session_id = Some("title-test".to_string());
let result =
set_window_title_with_manager(&mut app, Some("parallel-task".to_string()), &manager);
assert!(!result.is_error, "unexpected error: {:?}", result.message);
assert_eq!(app.window_title.as_deref(), Some("parallel-task"));
// `/rename`-style session name stays untouched.
assert_eq!(app.session_title, None);
let reloaded = manager.load_session("title-test").unwrap();
assert_eq!(reloaded.window_title.as_deref(), Some("parallel-task"));
assert_eq!(reloaded.metadata.title, "Original Name");
}
#[test]
fn clear_title_removes_the_session_level_title() {
let tmp = TempDir::new().unwrap();
let manager = make_session_manager(&tmp);
let mut app = make_app(&tmp);
let mut session = create_saved_session_with_mode(
&[],
"deepseek-v4-pro",
tmp.path(),
0,
None,
Some("agent"),
);
session.metadata.id = "title-clear".to_string();
session.window_title = Some("stale".to_string());
manager.save_session(&session).unwrap();
app.current_session_id = Some("title-clear".to_string());
let result = set_window_title_with_manager(&mut app, None, &manager);
assert!(!result.is_error, "unexpected error: {:?}", result.message);
assert_eq!(app.window_title, None);
assert_eq!(
manager.load_session("title-clear").unwrap().window_title,
None
);
}
#[test]
fn empty_argument_reports_the_effective_title() {
let tmp = TempDir::new().unwrap();
let mut app = make_app(&tmp);
app.title_default = Some("workspace-x".to_string());
let result = set_window_title(&mut app, None);
assert!(!result.is_error);
assert!(result.message.unwrap().contains("[workspace-x]"));
app.window_title = Some("session-specific".to_string());
let result = set_window_title(&mut app, None);
assert!(!result.is_error);
assert!(result.message.unwrap().contains("[session-specific]"));
}
#[test]
fn oversized_title_is_rejected() {
let tmp = TempDir::new().unwrap();
let mut app = make_app(&tmp);
app.current_session_id = Some("any".to_string());
let long = "x".repeat(MAX_TITLE_LEN + 1);
let result = set_window_title(&mut app, Some(&long));
assert!(result.is_error);
assert!(result.message.unwrap().contains("Title too long"));
super::rename::rename(app, Some(title))
}
}
-9
View File
@@ -2559,14 +2559,6 @@ pub struct Config {
/// Optional extra HTTP headers sent to model API requests.
#[serde(alias = "httpHeaders")]
pub http_headers: Option<HashMap<String, String>>,
/// Optional user-facing tab/window title shown as `[title] …` in front of
/// the terminal window title (the `Codewhale` / `reasoning…` / `done.`
/// states). This is the default for every session in this config scope;
/// the `/title` command overrides it per session, and `/config title …
/// --save` persists a new default here. Multi-window setups can point each
/// workspace at its own `--config` file (or profile) so alt-tabbed
/// sessions are identifiable at a glance.
pub title: Option<String>,
#[serde(alias = "defaultTextModel")]
pub default_text_model: Option<String>,
#[serde(alias = "authMode")]
@@ -9731,7 +9723,6 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
}
},
mini_window: override_cfg.mini_window.or(base.mini_window),
title: override_cfg.title.or(base.title),
}
}
-33
View File
@@ -963,39 +963,6 @@ fn workflow_config_defaults_when_omitted_and_overrides_round_trip() {
assert_eq!(merged.workflow_config(), workflow);
}
#[test]
fn window_title_config_parses_and_overlays() {
// `title` is a plain optional root key: absent config → `None`.
let omitted: Config = toml::from_str("").expect("empty config");
assert_eq!(omitted.title, None);
let config: Config = toml::from_str(r#"title = "workspace-x""#).expect("parse title config");
assert_eq!(config.title.as_deref(), Some("workspace-x"));
// Overlay wins over the base when both define a title.
let merged = merge_config(
Config {
title: Some("base-title".to_string()),
..Config::default()
},
Config {
title: Some("override-title".to_string()),
..Config::default()
},
);
assert_eq!(merged.title.as_deref(), Some("override-title"));
// Base title survives an overlay that does not mention it.
let merged = merge_config(
Config {
title: Some("base-title".to_string()),
..Config::default()
},
Config::default(),
);
assert_eq!(merged.title.as_deref(), Some("base-title"));
}
#[test]
fn search_provider_defaults_to_firecrawl() {
assert_eq!(SearchProvider::default(), SearchProvider::Firecrawl);
-1
View File
@@ -232,7 +232,6 @@ fn saved_session_with_blocks(blocks: Vec<crate::models::ContentBlock>) -> SavedS
context_references: Vec::new(),
artifacts: Vec::new(),
work_state: None,
window_title: None,
last_auto_route: None,
}
}
-10
View File
@@ -536,12 +536,6 @@ pub struct SavedSession {
/// To-do and plan state shown in the Work sidebar.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_state: Option<SessionWorkState>,
/// User-configured tab/window title for this session (`/title`), shown as
/// `[title] …` in front of the terminal window title. Optional for
/// backward-compatible session loads; absent sessions use the `title`
/// config default instead.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub window_title: Option<String>,
/// Most recent accepted/completed Auto decision, when the saved model mode
/// is `auto`. Optional for backward-compatible session loads.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -681,7 +675,6 @@ impl SavedSession {
context_references: Vec::new(),
artifacts: Vec::new(),
work_state: None,
window_title: None,
last_auto_route: None,
})
}
@@ -1772,7 +1765,6 @@ pub fn create_saved_session_with_id_and_mode(
context_references: Vec::new(),
artifacts: Vec::new(),
work_state: None,
window_title: None,
last_auto_route: None,
}
}
@@ -2334,7 +2326,6 @@ mod tests {
context_references: Vec::new(),
artifacts: Vec::new(),
work_state: None,
window_title: None,
last_auto_route: None,
};
manager.save_session(&session).expect("save");
@@ -2374,7 +2365,6 @@ mod tests {
context_references: Vec::new(),
artifacts: Vec::new(),
work_state: None,
window_title: None,
last_auto_route: None,
};
manager.save_session(&session).expect("save empty");
+3 -19
View File
@@ -1976,17 +1976,6 @@ pub struct App {
/// Updated when `EngineEvent::SessionUpdated` fires or a saved session is loaded.
pub session_title: Option<String>,
/// User-configured tab/window title for the current session, shown as
/// `[title] …` in front of the terminal window title. Set with the
/// `/title` command and persisted on the saved session; distinct from
/// [`session_title`](Self::session_title), which is the session *name*
/// shown in the composer border and session picker.
pub window_title: Option<String>,
/// Default tab/window title from the `title` config key (or a profile
/// overlay). Used when the current session has no explicit
/// [`window_title`](Self::window_title).
pub title_default: Option<String>,
/// Post-turn receipt rendered as transient composer chrome.
/// Set when a turn completes; cleared when a new turn starts or after expiry.
pub receipt_text: Option<String>,
@@ -4496,17 +4485,12 @@ impl App {
)
}
/// Resolve the `[title] …` window-title prefix for the terminal title.
///
/// Precedence: the session-level `/title` override wins over the `title`
/// config default; neither configured means no prefix (the historical
/// window titles stay byte-for-byte unchanged).
/// Resolve the current session name used to identify the terminal tab.
#[must_use]
pub(crate) fn window_title_prefix(&self) -> Option<&str> {
self.window_title
self.session_title
.as_deref()
.or(self.title_default.as_deref())
.filter(|prefix| !prefix.trim().is_empty())
.filter(|title| !title.trim().is_empty())
}
/// Bridge the centralized policy into transcript renderers that still
-2
View File
@@ -989,8 +989,6 @@ impl App {
mention_menu_behavior: settings.mention_menu_behavior.clone(),
workspace_follow_symlinks: settings.workspace_follow_symlinks,
session_title: None,
window_title: None,
title_default: config.title.clone(),
receipt_text: None,
receipt_started_at: None,
tool_evidence: Vec::new(),
+33 -9
View File
@@ -448,11 +448,25 @@ fn taskbar_progress_sequence(state: u8, progress: Option<u8>) -> String {
}
}
/// Build the OSC 0 window-title sequence. Split from the write for the same
/// reason as [`taskbar_progress_sequence`].
const MAX_TERMINAL_TITLE_CHARS: usize = 160;
/// Build a bounded OSC 0 window-title sequence. User-controlled session names
/// can reach this boundary, so control and bidi-format characters are removed
/// before the title is embedded in a terminal escape sequence.
#[must_use]
fn terminal_title_sequence(title: &str) -> String {
format!("\x1b]0;{title}\x07")
let safe: String = title
.chars()
.filter(|ch| {
!ch.is_control()
&& !matches!(
*ch,
'\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'
)
})
.take(MAX_TERMINAL_TITLE_CHARS)
.collect();
format!("\x1b]0;{safe}\x07")
}
/// Whether raw terminal control sequences may be written to stdout.
@@ -487,11 +501,8 @@ pub fn clear_taskbar_progress() {
set_taskbar_progress(0, None);
}
/// User-configured window-title prefix, rendered as `[prefix] …` in front of
/// every terminal window title. Empty means no prefix — the historical
/// byte-for-byte behavior. Set via the `/title` command (session level) or
/// the `title` config key (default level); the render loop syncs it here
/// through [`set_title_prefix`].
/// Current session name, mirrored by the render loop so background title
/// updates can identify their session without duplicating session state.
static TITLE_PREFIX: OnceLock<Mutex<String>> = OnceLock::new();
pub(crate) fn title_prefix_slot() -> &'static Mutex<String> {
@@ -510,7 +521,7 @@ pub(crate) fn title_prefix_test_lock() -> std::sync::MutexGuard<'static, ()> {
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
/// Set the `[prefix] …` window-title prefix, or clear it with `None`/empty.
/// Mirror the current session name into the background title renderer.
///
/// Change detection keeps the per-frame render-loop sync free when the title
/// did not move; on an actual change the running title is redrawn immediately
@@ -1536,6 +1547,19 @@ mod tests {
);
}
#[test]
fn terminal_title_sequence_strips_control_and_bidi_injection() {
assert_eq!(
terminal_title_sequence("safe\u{1b}]2;owned\u{7}\u{202e}title"),
"\x1b]0;safe]2;ownedtitle\x07"
);
let oversized = "x".repeat(MAX_TERMINAL_TITLE_CHARS + 20);
assert_eq!(
terminal_title_sequence(&oversized),
format!("\x1b]0;{}\x07", "x".repeat(MAX_TERMINAL_TITLE_CHARS))
);
}
#[test]
fn kitty_escape_uses_st_terminator() {
let out = capture(Method::Kitty, false, "done", 0, 1);
-1
View File
@@ -2540,7 +2540,6 @@ pub(crate) fn apply_loaded_session(
app.current_session_metadata = Some(session.metadata.clone());
app.session_artifacts = session.artifacts.clone();
app.session_title = Some(session.metadata.title.clone());
app.window_title = session.window_title.clone();
app.workspace_context = None;
app.workspace_context_refreshed_at = None;
if let Some(sp) = session.system_prompt.as_ref() {
-1
View File
@@ -434,7 +434,6 @@ pub(crate) fn build_session_snapshot(
session.artifacts = app.session_artifacts.clone();
session.work_state = work_state;
session.last_auto_route = app.auto_route_for_persistence();
session.window_title.clone_from(&app.window_title);
app.current_session_metadata = Some(session.metadata.clone());
// Claim ownership of this session for the process. From here on the
// Runtime API refuses external renames/archives of it with a typed 409
-1
View File
@@ -5415,7 +5415,6 @@ fn saved_session_with_messages(messages: Vec<Message>) -> SavedSession {
context_references: Vec::new(),
artifacts: Vec::new(),
work_state: None,
window_title: None,
last_auto_route: None,
}
}
+7 -20
View File
@@ -515,9 +515,8 @@ pub(crate) fn sync_title_activity(app: &App) {
crate::tui::notifications::set_title_motion_enabled(
app.motion_policy().allows_decorative() && app.status_indicator != "off",
);
// Keep the `[title] …` window-title prefix in step with the session and
// config defaults; change detection inside makes this free when nothing
// moved.
// Keep the terminal tab identity in step with the existing session name;
// change detection inside makes this free when nothing moved.
crate::tui::notifications::set_title_prefix(app.window_title_prefix());
if app.is_loading
|| matches!(
@@ -1729,27 +1728,15 @@ mod tests {
}
#[test]
fn window_title_prefix_prefers_session_over_config_default() {
fn window_title_prefix_uses_existing_session_name() {
let mut app = test_app();
// Nothing configured: no prefix at all.
assert_eq!(app.window_title_prefix(), None);
// Config default alone.
app.title_default = Some("workspace-x".to_string());
assert_eq!(app.window_title_prefix(), Some("workspace-x"));
// Session-level `/title` wins over the config default.
app.window_title = Some("task-7".to_string());
app.session_title = Some("task-7".to_string());
assert_eq!(app.window_title_prefix(), Some("task-7"));
// Clearing the session title falls back to the default.
app.window_title = None;
assert_eq!(app.window_title_prefix(), Some("workspace-x"));
// Whitespace-only titles count as unset.
app.window_title = Some(" ".to_string());
app.session_title = Some(" ".to_string());
assert_eq!(app.window_title_prefix(), None);
app.window_title = None;
}
#[test]
@@ -1759,7 +1746,7 @@ mod tests {
let _guard = crate::tui::notifications::title_prefix_test_lock();
crate::tui::notifications::set_title_prefix(None);
let mut app = test_app();
app.window_title = Some("sync-check".to_string());
app.session_title = Some("sync-check".to_string());
sync_title_activity(&app);
assert_eq!(
crate::tui::notifications::title_prefix_slot()
@@ -1768,7 +1755,7 @@ mod tests {
.as_str(),
"sync-check"
);
app.window_title = None;
app.session_title = None;
sync_title_activity(&app);
assert_eq!(
crate::tui::notifications::title_prefix_slot()