docs(readme): state the real Tab/Shift+Tab and undo/restore contracts

The one-paragraph TUI tour in README.md and all nine translations taught two
false preconditions and the wrong verb for undo.

Tab: the README said "when the composer is idle". The gate is EMPTINESS, not
idleness — crates/tui/src/tui/ui.rs:6978 does `if !app.input.is_empty() {
continue; }` immediately before `app.cycle_mode()`, and the comment at
ui.rs:6974-6977 explicitly denies the idle/running distinction ("Tab is
completion when the composer has content and a mode switch only when it is
empty ... so Tab never changes roles based on whether a turn happens to be
running"). A user with typed text and no turn running got completion, not a
mode switch.

Shift+Tab: the README applied the same "idle" qualifier. Shift+Tab has no
composer precondition at all — ui.rs:6363-6367 gates only on the modal stack,
per the comment at ui.rs:6358-6359 ("Shift+Tab is a shell-level permission
control. Keep it live in the composer.").

/restore: "undoes a turn" conflated two commands and described neither. Bare
`/restore` lists snapshots and reverts nothing
(crates/tui/src/commands/groups/skills/restore.rs:33-42 returns
`format_listing(&snapshots)` and returns early); `/restore <N>` reverts
workspace files only ("conversation history is unchanged", restore.rs:104-108).
The verb that undoes a turn is `/undo`
(crates/tui/src/commands/groups/debug/undo.rs:9-16). The product's own help
string already had this right — crates/tui/locales/en.json:287.

Also in this commit, because the translation stamp couples them:

- docs/KEYBINDINGS.md:18 inherited the same wrong "idle" qualifier on the
  Shift+Tab row; KEYBINDINGS.md:17 was already correct for Tab.
- README.id.md was stamped in sync but was missing the docs/AUTHORIZATION_ORDER
  and docs/HOOKS "Learn more" bullets that README.md:87-92 and the other eight
  translations carry, and its language switcher omitted Русский and Українська.
  Provenance: 171f0b2a2 touched README.md plus the eight files in the checker's
  TRANSLATIONS list and skipped README.id.md; 92128e669 then refreshed the stamp
  wholesale, so the marker certified a sync that did not exist.
- scripts/check-readme-translations.py checked only eight of the nine shipped
  translations. README.id.md was absent from both TRANSLATIONS and the
  LANGUAGE_LINKS exemption set, which is exactly how the gap above survived CI.
  Adding it makes the gate report "9 translations in sync".
- README.ru.md:24 listed [Українська](README.uk.md) twice.

The gate was already red before this commit: cc20f407f added the Discord link
and badge to README.md and to every translation body but never re-stamped, so
all eight checked files reported stale. All nine are now re-stamped to
sha256:a7f294018f28.

Verified: `python3 scripts/check-readme-translations.py` exit=0 ("9
translations in sync"), `bash scripts/check-readme-locales.sh` exit=0 (PASS).
This commit is contained in:
Hmbown
2026-08-03 22:44:24 -07:00
parent 3e3487c7f5
commit 426d610c14
13 changed files with 58 additions and 349 deletions
+8 -5
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
Un agente de programación de código abierto para tu terminal — trae tu propio modelo.
@@ -61,10 +61,13 @@ compatibilidad `codewhale cloud` sigue siendo aceptado.
Los tokens y los valores de las keys de proveedor nunca se imprimen.
En la TUI: `/model` cambia proveedor y modelo juntos, `/fleet` ejecuta un
equipo de workers y `/restore` deshace un turno. Cuando el compositor está
inactivo, `Tab` cicla entre Plan / Act / Operate y `Shift+Tab` cicla la postura
de permiso Ask / Auto-Review / Full Access. `!` ejecuta un comando de shell por
la ruta normal de aprobación.
equipo de workers, `/undo` deshace el último turno y `/restore <N>` revierte el
workspace a una instantánea anterior (`/restore` sin argumentos solo las
lista). Cuando el compositor está vacío, `Tab` cicla entre Plan / Act /
Operate; con texto escrito, `Tab` completa comandos slash y menciones `@`.
`Shift+Tab` cicla la postura de permiso Ask / Auto-Review / Full Access en
cualquier momento. `!` ejecuta un comando de shell por la ruta normal de
aprobación.
## Qué hace
+5 -3
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
Sebuah coding agent sumber terbuka untuk terminal Anda — bawa model pilihan Anda sendiri.
@@ -9,7 +9,7 @@ Berikan penyedia, model, dan tugas: Codewhale akan membaca kode Anda, mengedit b
Kami selalu membuka kesempatan bagi para kontributor dan cara untuk terus berkembang. Jika model atau penyedia yang Anda gunakan belum tersedia, atau ada hal yang tidak berjalan semestinya, memberi tahu kami adalah salah satu kontribusi paling berharga yang bisa Anda lakukan — lihat [Kontribusi](#kontribusi).
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
[![CI](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml/badge.svg)](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/codewhale-cli?label=crates.io)](https://crates.io/crates/codewhale-cli)
@@ -38,7 +38,7 @@ codewhale web # local browser client on 127.0.0.1
Autentikasi penyedia dan autentikasi akun Codewhale merupakan dua hal yang terpisah. `codewhale auth` mengonfigurasi model yang digunakan oleh runtime lokal. `codewhale account login` membuka browser sistem, menyelesaikan alur perangkat (device flow) di `app.codewhale.net`, lalu menyimpan sesi yang dihasilkan ke dalam credential manager OS. Gunakan `codewhale account status`, `codewhale account logout`, dan `codewhale account keys` untuk memeriksa profil yang sedang masuk atau mengelola kredensial BYOK berlingkup akun; prefiks kompatibilitas `codewhale cloud` tetap diterima. Token dan nilai kunci penyedia tidak pernah ditampilkan.
Di dalam TUI: `/model` mengganti penyedia dan model sekaligus, `/fleet` menjalankan tim pekerja (workers), dan `/restore` membatalkan satu langkah (turn). Saat composer dalam keadaan diam (idle), `Tab` beralih antar mode Plan / Act / Operate dan `Shift+Tab` beralih antar postur izin Ask / Auto-Review / Full Access. `!` menjalankan perintah shell melalui alur persetujuan normal.
Di dalam TUI: `/model` mengganti penyedia dan model sekaligus, `/fleet` menjalankan tim pekerja (workers), `/undo` membatalkan langkah (turn) terakhir, dan `/restore <N>` mengembalikan workspace ke snapshot sebelumnya (`/restore` tanpa argumen hanya menampilkan daftarnya). Saat composer kosong, `Tab` beralih antar mode Plan / Act / Operate; bila composer berisi teks, `Tab` justru melengkapi perintah slash dan sebutan `@`. `Shift+Tab` beralih antar postur izin Ask / Auto-Review / Full Access kapan saja. `!` menjalankan perintah shell melalui alur persetujuan normal.
## Fitur & Kapabilitas
@@ -52,6 +52,8 @@ Di dalam TUI: `/model` mengganti penyedia dan model sekaligus, `/fleet` menjalan
- [docs/FLEET.id.md](docs/FLEET.id.md) ([English](docs/FLEET.md)) — fleet, ledger, dan kelanjutan sesi (resume)
- [docs/WORKFLOW_EXPERIMENTAL_SEARCH.md](docs/WORKFLOW_EXPERIMENTAL_SEARCH.md) — pencarian eksperimental yang dibekukan dan netral terhadap penyedia di dalam Workflow
- [docs/CONFIGURATION.id.md](docs/CONFIGURATION.id.md) ([English](docs/CONFIGURATION.md)) — `config.toml`, hooks, dan konstitusi
- [docs/AUTHORIZATION_ORDER.md](docs/AUTHORIZATION_ORDER.md) — bagaimana mode, hooks, aturan izin, batas keamanan, hukum repositori, persetujuan, dan sandbox saling menyusun
- [docs/HOOKS.md](docs/HOOKS.md) — sebelas event hook siklus hidup TUI, payload-nya, dan tiga di antaranya yang dapat mengarahkan sebuah turn (`codewhale exec` dan subperintah CLI tidak memicu hooks)
- [docs/WEB.id.md](docs/WEB.id.md) ([English](docs/WEB.md)) — klien browser berbasis loopback-only dan batas autentikasi sekali pakainya
- [docs/LOCALIZATION.id.md](docs/LOCALIZATION.id.md) ([English](docs/LOCALIZATION.md)) — matriks lokalisasi & panduan terjemahan
+2 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
ターミナルで動くオープンソースのコーディングエージェント — モデルはあなたが持ち込む。
@@ -38,7 +38,7 @@ codewhale web # local browser client on 127.0.0.1
プロバイダ認証と Codewhale アカウント認証は別のものです。`codewhale auth` はローカルランタイムが使うモデルを設定します。`codewhale account login` はシステムブラウザを開き、`app.codewhale.net` でデバイスフローを完了して、得られたセッションを OS の資格情報マネージャに保存します。サインイン中のプロファイルの確認や、アカウントスコープの BYOK 資格情報の管理には `codewhale account status``codewhale account logout``codewhale account keys` を使ってください。互換プレフィックスとして `codewhale cloud` も引き続き受け付けられます。トークンやプロバイダキーの値が表示されることはありません。
TUI では、`/model` がプロバイダとモデルをまとめて切り替え、`/fleet` がワーカーのチームを走らせ、`/restore` がターンを取り消します。入力欄がアイドル状態のとき、`Tab` は Plan / Act / Operate を順に切り替え、`Shift+Tab` は Ask / Auto-Review / Full Access の権限スタンスを順に切り替えます。`!` は Shell コマンドを通常の承認経路で実行します。
TUI では、`/model` がプロバイダとモデルをまとめて切り替え、`/fleet` がワーカーのチームを走らせ、`/undo` が直前のターンを取り消し、`/restore <N>` がワークスペースを以前のスナップショットへ巻き戻します(引数なしの `/restore` は一覧を表示するだけです)。入力欄が空のとき、`Tab` は Plan / Act / Operate を順に切り替えます。入力欄に文字があるときの `Tab` はスラッシュコマンドと `@` メンションの補完になります。`Shift+Tab`いつでも Ask / Auto-Review / Full Access の権限スタンスを順に切り替えます。`!` は Shell コマンドを通常の承認経路で実行します。
## できること
+2 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
터미널에서 쓰는 오픈소스 코딩 에이전트 — 모델은 당신이 가져옵니다.
@@ -38,7 +38,7 @@ codewhale web # local browser client on 127.0.0.1
프로바이더 인증과 Codewhale 계정 인증은 별개입니다. `codewhale auth`는 로컬 런타임이 사용하는 모델을 구성합니다. `codewhale account login`은 시스템 브라우저를 열어 `app.codewhale.net`에서 디바이스 플로우를 완료하고, 그 결과로 만들어진 세션을 OS 자격 증명 관리자에 저장합니다. `codewhale account status`, `codewhale account logout`, `codewhale account keys`로 로그인된 프로필을 확인하거나 계정 범위의 BYOK 자격 증명을 관리할 수 있으며, 호환용 접두어 `codewhale cloud`도 계속 사용할 수 있습니다. 토큰과 프로바이더 키 값은 절대 출력되지 않습니다.
TUI 안에서: `/model`은 프로바이더와 모델을 함께 전환하고, `/fleet`은 워커 팀을 실행하며, `/restore`는 한 턴을 되돌립니다. 입력창이 유휴 상태일`Tab`은 Plan / Act / Operate 모드를 순환하고, `Shift+Tab`은 Ask / Auto-Review / Full Access 권한 태세를 순환합니다. `!`는 일반 승인 경로를 거쳐 셸 명령을 실행합니다.
TUI 안에서: `/model`은 프로바이더와 모델을 함께 전환하고, `/fleet`은 워커 팀을 실행하며, `/undo`는 직전 턴을 되돌리고, `/restore <N>`은 워크스페이스를 이전 스냅샷으로 되돌립니다(인자 없는 `/restore`는 스냅샷 목록만 보여줍니다). 입력창이 비어 있을`Tab`은 Plan / Act / Operate 모드를 순환하고, 입력창에 내용이 있으면 `Tab`은 슬래시 명령과 `@` 멘션을 자동 완성합니다. `Shift+Tab` 언제든지 Ask / Auto-Review / Full Access 권한 태세를 순환합니다. `!`는 일반 승인 경로를 거쳐 셸 명령을 실행합니다.
## 기능
+7 -5
View File
@@ -56,11 +56,13 @@ manager. Use `codewhale account status`, `codewhale account logout`, and
account-scoped BYOK credentials; the compatibility prefix `codewhale cloud`
remains accepted. Tokens and provider-key values are never printed.
In the TUI: `/model` switches provider and model together, `/fleet` runs a
team of workers, and `/restore` undoes a turn. When the composer is idle, `Tab`
cycles Plan / Act / Operate and `Shift+Tab` cycles the Ask / Auto-Review / Full
Access permission posture. `!` runs a shell command through the normal approval
path.
In the TUI: `/model` switches provider and model together, `/fleet` runs a team
of workers, `/undo` reverts the last turn, and `/restore <N>` rolls the
workspace back to an earlier snapshot (bare `/restore` lists them). `Tab`
cycles Plan / Act / Operate when the composer is empty — with text in it, `Tab`
completes slash commands and `@` mentions instead. `Shift+Tab` cycles the
Ask / Auto-Review / Full Access permission posture at any time. `!` runs a
shell command through the normal approval path.
## What it does
+7 -5
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
Um agente de programação de código aberto para o seu terminal — traga o seu próprio modelo.
@@ -60,10 +60,12 @@ compatibilidade `codewhale cloud` continua aceito. Tokens e valores de chaves de
provedor nunca são exibidos.
Na TUI: `/model` troca provedor e modelo juntos, `/fleet` executa uma equipe
de workers e `/restore` desfaz um turno. Quando o compositor está ocioso, `Tab`
cicla entre Plan / Act / Operate e `Shift+Tab` cicla a postura de permissão Ask
/ Auto-Review / Full Access. `!` executa um comando de shell pelo caminho normal
de aprovação.
de workers, `/undo` desfaz o último turno e `/restore <N>` reverte o workspace
para um snapshot anterior (`/restore` sem argumento apenas os lista). Quando o
compositor está vazio, `Tab` cicla entre Plan / Act / Operate; com texto
digitado, `Tab` completa comandos slash e menções `@`. `Shift+Tab` cicla a
postura de permissão Ask / Auto-Review / Full Access a qualquer momento. `!`
executa um comando de shell pelo caminho normal de aprovação.
## O que faz
+8 -6
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
Открытый агент для программирования в вашем терминале — модель приносите с собой.
@@ -21,7 +21,7 @@ Codewhale начинался как нативный клиент для DeepSee
одно из самых полезных действий с вашей стороны: см.
[Участие в проекте](#участие-в-проекте).
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Українська](README.uk.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
[![CI](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml/badge.svg)](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/codewhale-cli?label=crates.io)](https://crates.io/crates/codewhale-cli)
@@ -62,10 +62,12 @@ codewhale web # local browser client on 127.0.0.1
никогда не выводятся.
В TUI: `/model` переключает провайдера и модель одновременно, `/fleet` запускает
команду воркеров, а `/restore` отменяет ход. Когда поле ввода свободно, `Tab`
циклически переключает режимы Plan / Act / Operate, а `Shift+Tab` — уровни прав
Ask / Auto-Review / Full Access. `!` запускает команду оболочки через обычный
путь подтверждения.
команду воркеров, `/undo` отменяет последний ход, а `/restore <N>` откатывает
рабочую копию к более раннему снимку (`/restore` без аргумента только выводит их
список). Когда поле ввода пустое, `Tab` циклически переключает режимы Plan /
Act / Operate; если в поле есть текст, `Tab` дополняет слэш-команды и упоминания
`@`. `Shift+Tab` переключает уровни прав Ask / Auto-Review / Full Access в любой
момент. `!` запускает команду оболочки через обычный путь подтверждения.
## Что он умеет
+7 -5
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
Агент для програмування з відкритим кодом у вашому терміналі — модель приносите ви.
@@ -62,10 +62,12 @@ codewhale web # local browser client on 127.0.0.1
значення ключів провайдерів ніколи не виводяться.
У TUI: `/model` перемикає провайдера й модель разом, `/fleet` запускає команду
працівників, а `/restore` скасовує крок. Коли поле введення неактивне, `Tab`
циклічно перемикає Plan / Act / Operate, а `Shift+Tab` — режими дозволів
Ask / Auto-Review / Full Access. `!` виконує команду оболонки через звичайний
шлях затвердження.
працівників, `/undo` скасовує останній крок, а `/restore <N>` відкочує робочу
копію до давнішого знімка (`/restore` без аргументу лише виводить їхній
список). Коли поле введення порожнє, `Tab` циклічно перемикає Plan / Act /
Operate; якщо в полі є текст, `Tab` доповнює слеш-команди та згадки `@`.
`Shift+Tab` перемикає режими дозволів Ask / Auto-Review / Full Access будь-коли.
`!` виконує команду оболонки через звичайний шлях затвердження.
## Що він уміє
+7 -5
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
Một coding agent mã nguồn mở cho terminal của bạn — mang theo model của riêng bạn.
@@ -59,10 +59,12 @@ xác thực BYOK gắn với tài khoản; tiền tố tương thích `codewhale
được chấp nhận. Token và giá trị key của provider không bao giờ được in ra.
Trong TUI: `/model` đổi provider và model cùng lúc, `/fleet` chạy một đội
worker, `/restore` hoàn tác một lượt. Khi vùng soạn thảo đang rảnh, `Tab`
chuyển vòng qua Plan / Act / Operate và `Shift+Tab` chuyển vòng qua tư thế
quyền Ask / Auto-Review / Full Access. `!` chạy một lệnh shell qua đường phê
duyệt bình thường.
worker, `/undo` hoàn tác lượt gần nhất, và `/restore <N>` đưa workspace về một
ảnh chụp trước đó (`/restore` không tham số chỉ liệt kê chúng). Khi vùng soạn
thảo trống, `Tab` chuyển vòng qua Plan / Act / Operate; khi vùng soạn thảo có
chữ, `Tab` lại hoàn tất lệnh slash và nhắc `@`. `Shift+Tab` chuyển vòng qua tư
thế quyền Ask / Auto-Review / Full Access bất cứ lúc nào. `!` chạy một lệnh
shell qua đường phê duyệt bình thường.
## Tính năng
+2 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:4d02e29bfb3a -->
<!-- source: README.md sha256:a7f294018f28 -->
# Codewhale
一个面向终端的开源编程智能体——模型由你自带。
@@ -38,7 +38,7 @@ codewhale web # local browser client on 127.0.0.1
Provider 身份验证与 Codewhale 账户身份验证是彼此独立的。`codewhale auth` 用于配置本地运行时所使用的模型。`codewhale account login` 会打开系统浏览器,在 `app.codewhale.net` 完成设备授权流程,并将得到的会话存入操作系统的凭据管理器。使用 `codewhale account status``codewhale account logout``codewhale account keys` 可以查看已登录的账户信息或管理账户级的 BYOK 凭据;兼容前缀 `codewhale cloud` 仍然可用。令牌和 provider key 的值永远不会被打印出来。
在 TUI 中:`/model` 同时切换 provider 和模型,`/fleet` 运行一组 worker,`/restore` 撤销某一轮。输入区空时,`Tab` 在 Plan / Act / Operate 之间循环切换,`Shift+Tab` 在 Ask / Auto-Review / Full Access 权限姿态之间循环切换`!` 让 shell 命令经由正常的审批路径运行。
在 TUI 中:`/model` 同时切换 provider 和模型,`/fleet` 运行一组 worker,`/undo` 撤销上一轮,`/restore <N>` 把工作区回滚到更早的快照(不带参数的 `/restore` 只列出快照)。输入区空时,`Tab` 在 Plan / Act / Operate 之间循环切换;输入区有内容时,`Tab` 改为补全斜杠命令和 `@` 提及。`Shift+Tab`任何时候都能循环切换 Ask / Auto-Review / Full Access 权限姿态。`!` 让 shell 命令经由正常的审批路径运行。
## 功能
-308
View File
@@ -1,308 +0,0 @@
//! The persistent Sessions rail (#2934).
//!
//! A bounded, workspace-scoped list of recent sessions that lives in the
//! sidebar panel stack alongside Work, Activity, Agents, and Context. It is a
//! *jump* affordance, not a second session browser: every row hands off to the
//! existing [`crate::tui::session_picker::SessionPickerView`], which already
//! owns preview, search, sort, rename, archive, delete, workspace-scope
//! toggling, and the resume contract. Duplicating any of that here would give
//! us two behaviours to keep in sync and one of them would eventually lie.
//!
//! Two properties this module exists to guarantee:
//!
//! * **No filesystem work per frame.** Rows are read once into a
//! [`SessionsRailCache`] and reused until the TTL expires or a session
//! lifecycle event invalidates them. A sidebar that re-listed a 50-session
//! directory on every keystroke would be a visible stall.
//! * **No provider or network call.** Browsing sessions is offline by
//! construction — the rail only ever reads already-persisted metadata.
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crate::session_manager::{SessionListFilter, SessionManager, SessionMetadata};
use crate::session_projection::{
DEFAULT_RAIL_ROWS, SessionQuery, SessionSortMode, SessionSummary, count_sessions,
project_sessions,
};
/// How long cached rows stay warm. Long enough that scrolling and typing never
/// hit the disk, short enough that a session saved by this process or another
/// one shows up without the user doing anything.
pub const RAIL_CACHE_TTL: Duration = Duration::from_secs(20);
/// Rail rows plus the inputs they were computed from.
///
/// The inputs are stored so a workspace change or a different active session
/// invalidates the cache on its own, without every call site having to
/// remember to clear it.
#[derive(Debug, Clone)]
pub struct SessionsRailCache {
rows: Vec<SessionSummary>,
workspace: PathBuf,
current_session_id: Option<String>,
/// Total sessions in scope before the row cap, so the rail can say "8 of
/// 31" rather than implying it is showing everything.
total_in_scope: usize,
/// Row budget this cache was built for. A resized sidebar changes the
/// budget, so it participates in freshness — otherwise a grown panel would
/// render short rows until the TTL happened to lapse.
max_rows: usize,
read_at: Instant,
/// Set when the sessions directory could not be read. The rail renders
/// this instead of an empty list, so a permissions or path problem is not
/// silently displayed as "you have no sessions".
error: Option<String>,
}
impl SessionsRailCache {
#[must_use]
pub fn rows(&self) -> &[SessionSummary] {
&self.rows
}
#[must_use]
pub fn total_in_scope(&self) -> usize {
self.total_in_scope
}
#[must_use]
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
/// Is this cache still usable for the given surface state?
#[must_use]
pub fn is_fresh(
&self,
workspace: &Path,
current_session_id: Option<&str>,
max_rows: usize,
now: Instant,
) -> bool {
self.workspace == workspace
&& self.current_session_id.as_deref() == current_session_id
&& self.max_rows == max_rows
&& now.duration_since(self.read_at) < RAIL_CACHE_TTL
}
}
/// Build rail rows from an already-loaded metadata list.
///
/// Split out from [`load_rail_cache`] so the projection is testable without a
/// sessions directory, and so the rail and the picker demonstrably agree: both
/// go through [`project_sessions`].
#[must_use]
pub fn build_rail_cache(
sessions: &[SessionMetadata],
workspace: &Path,
current_session_id: Option<&str>,
max_rows: usize,
now: Instant,
) -> SessionsRailCache {
let scoped = SessionQuery::default()
.with_filter(SessionListFilter::ActiveOnly)
.with_sort(SessionSortMode::Recent)
.scoped_to(workspace);
// Count the full scoped set without building rows. Projecting with a huge
// limit would silently re-clamp at `MAX_PROJECTED_SESSIONS` and under-report
// the total, which is the one number this footer exists to tell the truth
// about.
let total_in_scope = count_sessions(sessions, &scoped);
let rows = project_sessions(sessions, &scoped.with_limit(max_rows), current_session_id);
SessionsRailCache {
rows,
workspace: workspace.to_path_buf(),
current_session_id: current_session_id.map(str::to_string),
total_in_scope,
max_rows,
read_at: now,
error: None,
}
}
/// Read the sessions directory and project rail rows.
///
/// Errors are captured into the cache rather than returned: a rail that cannot
/// read the store should say so once and keep rendering, not propagate a
/// failure into the render loop.
#[must_use]
pub fn load_rail_cache(
workspace: &Path,
current_session_id: Option<&str>,
max_rows: usize,
) -> SessionsRailCache {
let now = Instant::now();
let listed = SessionManager::default_location().and_then(|manager| manager.list_sessions());
match listed {
Ok(sessions) => build_rail_cache(&sessions, workspace, current_session_id, max_rows, now),
Err(err) => SessionsRailCache {
rows: Vec::new(),
workspace: workspace.to_path_buf(),
current_session_id: current_session_id.map(str::to_string),
total_in_scope: 0,
max_rows,
read_at: now,
error: Some(err.to_string()),
},
}
}
/// The command a rail row dispatches when activated.
///
/// Opening the picker preselected on the row — rather than resuming inline —
/// is deliberate. Resume has draft, unsaved-work, and confirmation semantics
/// that the picker already implements; a rail row that resumed directly would
/// either re-implement them or quietly skip them.
#[must_use]
pub fn row_command(session_id: &str) -> String {
format!("/sessions open {session_id}")
}
/// The command the rail's footer row dispatches: the full session browser.
#[must_use]
pub fn browse_all_command() -> &'static str {
"/sessions list"
}
/// Default row budget for a sidebar of `height` rows.
///
/// Narrow and short terminals get fewer rows rather than a clipped panel; the
/// footer still reports the full in-scope count so nothing is silently
/// dropped.
#[must_use]
pub fn rows_for_height(height: u16) -> usize {
// Two rows of chrome (border + title) and one footer row.
let usable = usize::from(height).saturating_sub(3);
usable.clamp(1, DEFAULT_RAIL_ROWS)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn metadata(id: &str, title: &str, workspace: &Path, minutes_ago: i64) -> SessionMetadata {
let ts = Utc::now() - chrono::Duration::minutes(minutes_ago);
SessionMetadata {
id: id.to_string(),
title: title.to_string(),
created_at: ts,
updated_at: ts,
message_count: 3,
total_tokens: 10,
model: "deepseek-chat".to_string(),
model_provider: "deepseek".to_string(),
model_provider_id: None,
workspace: workspace.to_path_buf(),
mode: Some("agent".to_string()),
cost: Default::default(),
parent_session_id: None,
forked_from_message_count: None,
cumulative_turn_secs: 0,
archived: false,
}
}
#[test]
fn rail_is_scoped_to_the_workspace_and_capped() {
let here = PathBuf::from("/repo-a");
let there = PathBuf::from("/repo-b");
let mut sessions: Vec<SessionMetadata> = (0..12)
.map(|i| metadata(&format!("a{i}"), &format!("Here {i}"), &here, i))
.collect();
sessions.push(metadata("elsewhere", "There", &there, 1));
let cache = build_rail_cache(&sessions, &here, None, 5, Instant::now());
assert_eq!(cache.rows().len(), 5);
assert_eq!(cache.total_in_scope(), 12);
assert!(
cache.rows().iter().all(|row| row.workspace == here),
"rail must not leak another workspace's sessions"
);
}
#[test]
fn archived_sessions_never_appear_in_the_rail() {
let here = PathBuf::from("/repo");
let mut archived = metadata("gone", "Archived", &here, 1);
archived.archived = true;
let sessions = vec![archived, metadata("live", "Live", &here, 2)];
let cache = build_rail_cache(&sessions, &here, None, 8, Instant::now());
assert_eq!(
cache
.rows()
.iter()
.map(|r| r.id.as_str())
.collect::<Vec<_>>(),
vec!["live"]
);
assert_eq!(cache.total_in_scope(), 1);
}
#[test]
fn the_active_session_is_marked_current() {
let here = PathBuf::from("/repo");
let sessions = vec![metadata("a", "A", &here, 1), metadata("b", "B", &here, 2)];
let cache = build_rail_cache(&sessions, &here, Some("b"), 8, Instant::now());
let current: Vec<&str> = cache
.rows()
.iter()
.filter(|r| r.is_current)
.map(|r| r.id.as_str())
.collect();
assert_eq!(current, vec!["b"]);
}
#[test]
fn cache_expires_on_ttl_workspace_session_or_row_budget_change() {
let here = PathBuf::from("/repo");
let now = Instant::now();
let cache = build_rail_cache(&[metadata("a", "A", &here, 1)], &here, Some("a"), 8, now);
assert!(cache.is_fresh(&here, Some("a"), 8, now));
assert!(
!cache.is_fresh(
&here,
Some("a"),
8,
now + RAIL_CACHE_TTL + Duration::from_secs(1)
),
"TTL must expire the cache"
);
assert!(
!cache.is_fresh(Path::new("/other"), Some("a"), 8, now),
"a workspace change must invalidate the cache"
);
assert!(
!cache.is_fresh(&here, Some("b"), 8, now),
"switching sessions must invalidate the cache"
);
assert!(
!cache.is_fresh(&here, Some("a"), 4, now),
"a resized sidebar changes the row budget and must invalidate the cache"
);
}
#[test]
fn row_activation_opens_the_picker_rather_than_resuming_inline() {
assert_eq!(row_command("abc123"), "/sessions open abc123");
assert_eq!(browse_all_command(), "/sessions list");
}
#[test]
fn narrow_and_short_sidebars_get_fewer_rows_but_never_zero() {
assert_eq!(rows_for_height(0), 1);
assert_eq!(rows_for_height(4), 1);
assert_eq!(rows_for_height(7), 4);
assert_eq!(rows_for_height(40), DEFAULT_RAIL_ROWS);
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ Global key chords are not yet user-configurable — tracked for a future release
| `Ctrl-B` | Move a supported foreground shell wait into `/jobs` so the turn can continue; use `/jobs` or `Bash` with `action: "wait"` to inspect it |
| `Ctrl-D` | Quit (only when the composer is empty) |
| `Tab` | When the composer is empty, cycle TUI mode: Plan → Act → Operate → Plan |
| `Shift+Tab` | When the composer is idle, cycle permission posture: Ask → Auto-Review → Full Access |
| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |
| `Ctrl-T` | Cycle reasoning effort for the active provider. DeepSeek-style providers cycle off → high → max → off; OpenAI Codex cycles low → medium → high → xhigh → low. |
| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |
| `Ctrl-R` | Open the resume-session picker |
+2
View File
@@ -26,6 +26,7 @@ TRANSLATIONS = [
"README.zh-CN.md",
"README.ja-JP.md",
"README.vi.md",
"README.id.md",
"README.ko-KR.md",
"README.es-419.md",
"README.pt-BR.md",
@@ -42,6 +43,7 @@ LANGUAGE_LINKS = {
"README.zh-CN.md",
"README.ja-JP.md",
"README.vi.md",
"README.id.md",
"README.ko-KR.md",
"README.es-419.md",
"README.pt-BR.md",