diff --git a/.gitignore b/.gitignore index f1bd836..ea63c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ dev.termherd.lock # Per-directory tool caches the impeccable skill drops beside the sources it # reads; they reappear on every run, so ignore rather than clean repeatedly. .impeccable/ + +# Serena MCP tool state (local) +.serena/ diff --git a/AGENTS.md b/AGENTS.md index 997e738..12d5d3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,17 +149,17 @@ into its `mcpServers` at spawn (loopback, per-session token) — so it can read and drive the workspace it runs in. This is the richer sibling of the capture dump above: same `WorkspaceSnapshot` model, live instead of a file. -**Settled.** Sixteen tools: `list_sessions` + `snapshot` +**Settled.** Seventeen tools: `list_sessions` + `snapshot` (perception), `open_session` / `split_pane` / `focus_pane` / `rename_tab` / `close_pane` / `run_in_session` / `mouse_in_session` (action), `wait_for_status` + `read_terminal` (synchronisation), `screenshot` (pixels), `press_keys` + `run_action` (the app's own keyboard), `add_repo` + `forget_repo` (membership — what the sidebar *contains*, as against what the -window draws). The loop they exist to -serve is **act → wait → observe**: `run_in_session` returns immediately, so -synchronise with `wait_for_status` and then `read_terminal`. Do **not** poll -`snapshot` in a loop — it races the transition you are watching for, which is -why the wait rung exists. +window draws), and `prompt_in_session` (the loop below, composed). The loop +they exist to serve is **act → wait → observe**: `run_in_session` returns +immediately, so synchronise with `wait_for_status` and then `read_terminal`. +Do **not** poll `snapshot` in a loop — it races the transition you are +watching for, which is why the wait rung exists. **That loop did not run until #236, and now does.** Every session used to sit on `starting`, so `wait_for_status` only ever settled by timing out. Two diff --git a/README.md b/README.md index 281bb2f..2235d7c 100644 --- a/README.md +++ b/README.md @@ -69,17 +69,22 @@ one for your platform: `.AppImage`, `chmod +x` it, and run it directly. Prefer a bare command-line binary? The same releases carry one-line installers -that drop `termherd` into your Cargo bin directory: +that drop `termherd` into your Cargo bin directory. Every release so far is a +**pre-release**, which GitHub's `/releases/latest/` shortcut skips, so name the +tag — the newest one is at the top of the +[Releases](https://github.com/Termherd/termherd/releases) page: ```bash # macOS / Linux +TAG=v0.1.0-prerelease.4 curl --proto '=https' --tlsv1.2 -LsSf \ - https://github.com/Termherd/termherd/releases/latest/download/termherd-installer.sh | sh + "https://github.com/Termherd/termherd/releases/download/$TAG/termherd-app-installer.sh" | sh ``` ```powershell # Windows -powershell -c "irm https://github.com/Termherd/termherd/releases/latest/download/termherd-installer.ps1 | iex" +$Tag = "v0.1.0-prerelease.4" +irm "https://github.com/Termherd/termherd/releases/download/$Tag/termherd-app-installer.ps1" | iex ``` ### Verify a Linux download @@ -89,12 +94,15 @@ Linux release binaries carry a sigstore *keyless* build-provenance attestation in the public Rekor transparency log). Verify a download with the `gh` CLI: ```bash -gh attestation verify termherd-x86_64-unknown-linux-gnu.tar.xz \ +gh attestation verify termherd-app-x86_64-unknown-linux-gnu.tar.xz \ --repo Termherd/termherd ``` A successful check proves both integrity and that the artifact was built by -this repository's CI. A `SHA256SUMS` file is also attached to each release. +this repository's CI. The attestation and its `SHA256SUMS` file come from a +signing step added after `v0.1.0-prerelease.4`, so that pre-release has +neither — only per-file `.sha256` checksums and a combined `sha256.sum`, which +every release carries. ## Run from source @@ -133,6 +141,9 @@ you want and strip the comments (the real file is strict JSON). In short: - `open` — the editor command a Ctrl/Cmd-clicked file path opens in, with `{path}` / `{line}` / `{col}` templates (default: the OS default handler, which cannot honour a line number). +- `mcp` — `allow_claude_nesting`: whether `prompt_in_session` may prompt + another *Claude* session over the live bridge (default `false`; a shell is + never gated). - `keys` — keyboard overrides, one chord or a list per action; the full action vocabulary and its default chords are listed in the template. @@ -180,17 +191,16 @@ where those keys produce `&`, `é`, … without Shift. Dragging with the mouse selects, and the wheel scrolls back through history. The two classic terminal clipboard gestures are off until you ask for them: `terminal.copy_on_select` makes a drag release (or a double-click) copy -outright, and `terminal.paste_on_right_click` makes a right-click paste into -the pane under the pointer. Left off, the copy chord reads whatever is -highlighted on screen. A full-screen program that reads the mouse — Claude -Code's `/diff` and `/resume`, vim, lazygit, less — owns it while it runs: -clicks, drags and the wheel go to the program, nothing is selected or pasted -locally, and Shift+drag takes the mouse back for a selection. The -copy chord does nothing in such a pane unless a Shift selection +outright, and `terminal.paste_on_right_click` makes a right-click paste into the +pane under the pointer. Left off, the copy chord copies the terminal's current +selection, even one scrolled out of view. A full-screen program that reads the +mouse — Claude Code's `/diff` and `/resume`, vim, lazygit, less — owns it while +it runs: clicks, drags and the wheel go to the program, nothing is selected or +pasted locally, and Shift+drag takes the mouse back for a selection. +The copy chord does nothing in such a pane unless a Shift selection exists, so a program's own clipboard write (Claude Code copies a drag on -release) is never overwritten. In -the sidebar, click a project or session to open it; a tab's `×` also closes -it. Hovering a tab shows the session's fuller +release) is never overwritten. In the sidebar, click a project or session to +open it; a tab's `×` also closes it. Hovering a tab shows the session's fuller description (the same card the sidebar shows). **+ Add a repo** puts a repository in the sidebar before it has any session — or drop its folder on the window, which does the same thing (a dropped *file* is ignored). @@ -234,6 +244,7 @@ nothing to configure. It exposes the running workspace: | `press_keys` · `run_action` | drive termherd's own interface — chords through the live keymap, or actions by name | | `mouse_in_session` | a mouse event at a cell of a terminal — forwarded to a program reading the mouse, else the terminal's own selection | | `add_repo` · `forget_repo` | put a repository in the sidebar before it has any session, and drop that addition | +| `prompt_in_session` | type, wait and read in one round trip — prompting another Claude session is opt-in | The loop that makes it useful is **act → wait → observe**: `run_in_session`, then `wait_for_status`, then `read_terminal`. Sessions are addressed by a @@ -252,13 +263,12 @@ app on one it cannot answer; a sidebar rename used to be the exception rename — those go through a widget callback no synthesised event reaches ([#246]). -Five follow-ups remain: a composed prompt→wait→read in one round trip -([#196]), `enter` on the renames ([#246]), a doc editor that discards unsaved -edits when it closes ([#248]), reaching the bridge from outside a session -termherd spawned — the launcher cannot drive it today ([#267]) — and the -pointer at TermHerd's own interface ([#301]): it reaches a session's terminal, -and through it a program reading the mouse, but not yet the sidebar, tabs or -gutters. +Four follow-ups remain: `enter` on the renames ([#246]), a doc editor that +discards unsaved edits when it closes ([#248]), reaching the bridge from +outside a session termherd spawned — the launcher cannot drive it today +([#267]) — and the pointer at TermHerd's own interface ([#301]): it reaches a +session's terminal, and through it a program reading the mouse, but not yet +the sidebar, tabs or gutters. ### The stdio server (manual) @@ -284,7 +294,6 @@ It speaks JSON-RPC over stdio. Register it with Claude Code by adding it to your Build the binary with `cargo build -p termherd-mcp` (it lands in `target/`). [#90]: https://github.com/Termherd/termherd/issues/90 -[#196]: https://github.com/Termherd/termherd/issues/196 [#237]: https://github.com/Termherd/termherd/issues/237 [#246]: https://github.com/Termherd/termherd/issues/246 [#248]: https://github.com/Termherd/termherd/issues/248 diff --git a/crates/app/src/shell/view/sidebar.rs b/crates/app/src/shell/view/sidebar.rs index 0ad5c91..05c78d8 100644 --- a/crates/app/src/shell/view/sidebar.rs +++ b/crates/app/src/shell/view/sidebar.rs @@ -116,7 +116,7 @@ impl Shell { } // A handle to collapse the sidebar, mirroring the one that // restores it from the main pane. - let hide = button(text("◀ Masquer le panneau").size(11)) + let hide = button(text(strings::SIDEBAR_HIDE).size(11)) .on_press(Message::ToggleSidebar) .style(button::text) .padding(0); diff --git a/crates/app/src/strings.rs b/crates/app/src/strings.rs index 7167bd8..557eb06 100644 --- a/crates/app/src/strings.rs +++ b/crates/app/src/strings.rs @@ -17,6 +17,7 @@ pub const RENAME_PLACEHOLDER: &str = "title…"; pub const SIDEBAR_LAUNCH_SHELL: &str = "Open a shell here"; pub const SIDEBAR_LAUNCH_CLAUDE: &str = "Start a fresh Claude session"; pub const SIDEBAR_SHOW_LESS: &str = "show less"; +pub const SIDEBAR_HIDE: &str = "◀ Hide"; pub const SIDEBAR_ADD_REPO: &str = "+ Add a repo"; pub const SIDEBAR_ADD_REPO_HINT: &str = "Pick a folder, or drop one on the window"; pub const SIDEBAR_FORGET_REPO: &str = "Remove this repo from the sidebar"; diff --git a/docs/book.toml b/docs/book.toml index 5ed7b60..1efa68e 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -7,8 +7,9 @@ src = "src" [build] build-dir = "book" -# A link to a page that does not exist must fail the build, not silently mint an -# empty page. The chapter map in SUMMARY.md is a promise; this keeps it honest. +# A SUMMARY.md entry for a page that does not exist must fail the build, not +# silently mint an empty page (in-page links are not checked). The chapter map +# in SUMMARY.md is a promise; this keeps it honest. create-missing = false [output.html] diff --git a/docs/settings.example.jsonc b/docs/settings.example.jsonc index cceb757..cf65b73 100644 --- a/docs/settings.example.jsonc +++ b/docs/settings.example.jsonc @@ -156,6 +156,14 @@ "scale": 0.5 }, + // ── MCP live bridge ────────────────────────────────────────────────────── + // Whether `prompt_in_session` may prompt another *Claude* session (an agent + // driving an agent). Shell sessions are never gated. A single call can also + // opt in with its own `allow_claude_nesting: true`. Default: false. + "mcp": { + "allow_claude_nesting": false + }, + // ── Keyboard overrides ─────────────────────────────────────────────────── // Each entry binds an action to one chord or a list of chords and REPLACES // that action's default; unlisted actions keep their per-platform default. diff --git a/docs/src/guide/installation.md b/docs/src/guide/installation.md index 869b8e6..2bf33b8 100644 --- a/docs/src/guide/installation.md +++ b/docs/src/guide/installation.md @@ -40,17 +40,21 @@ On Windows, SmartScreen may warn — choose **More info → Run anyway**. ## Bare command-line binary The same releases carry one-line installers that drop `termherd` into your -Cargo bin directory: +Cargo bin directory. Every release so far is a **pre-release**, which GitHub's +`/releases/latest/` shortcut skips, so name the tag — the newest one is at the +top of the [Releases](https://github.com/Termherd/termherd/releases) page: ```bash # macOS / Linux +TAG=v0.1.0-prerelease.4 curl --proto '=https' --tlsv1.2 -LsSf \ - https://github.com/Termherd/termherd/releases/latest/download/termherd-installer.sh | sh + "https://github.com/Termherd/termherd/releases/download/$TAG/termherd-app-installer.sh" | sh ``` ```powershell # Windows -powershell -c "irm https://github.com/Termherd/termherd/releases/latest/download/termherd-installer.ps1 | iex" +$Tag = "v0.1.0-prerelease.4" +irm "https://github.com/Termherd/termherd/releases/download/$Tag/termherd-app-installer.ps1" | iex ``` ## Verifying a Linux download @@ -60,12 +64,15 @@ attestation — no signing key; the signer is the release workflow itself, via GitHub OIDC, logged in the public Rekor transparency log. ```bash -gh attestation verify termherd-x86_64-unknown-linux-gnu.tar.xz \ +gh attestation verify termherd-app-x86_64-unknown-linux-gnu.tar.xz \ --repo Termherd/termherd ``` A passing check proves both integrity and that the artifact was built by this -repository's CI. A `SHA256SUMS` file is attached to each release as well. +repository's CI. The attestation and its `SHA256SUMS` file come from a signing +step added after `v0.1.0-prerelease.4`, so that pre-release has neither — only +per-file `.sha256` checksums and a combined `sha256.sum`, which every release +carries. ## From source diff --git a/docs/src/guide/quick-start.md b/docs/src/guide/quick-start.md index e5f17ae..d3a09a4 100644 --- a/docs/src/guide/quick-start.md +++ b/docs/src/guide/quick-start.md @@ -67,8 +67,9 @@ shows the matched line under the row so you can tell *why* it matched. See | Close the focused pane (a lone pane closes its tab) | Cmd+W | Ctrl+W | | Reopen the tab you just closed | Cmd+Shift+T | Ctrl+Shift+T | -Tabs also reorder by drag-and-drop. Splits resize by keyboard focus only for -now — drag-resize is the remaining piece of `F-terminal-split`. +Tabs also reorder by drag-and-drop. Split panes always share their space +evenly — resizing them, by keyboard or by drag, is the remaining piece of +`F-terminal-split`. ## 4. Watch what needs you diff --git a/docs/src/mcp/keyboard.md b/docs/src/mcp/keyboard.md index e747d77..48800e4 100644 --- a/docs/src/mcp/keyboard.md +++ b/docs/src/mcp/keyboard.md @@ -72,10 +72,11 @@ selection, and `copy` runs on it. - On **`quit-confirm`**, `enter` quits the app — killing every session and the connection you are speaking over. -- **`session-rename`** (the sidebar's inline ✎ field) answers to neither - `enter` nor the rename in the doc pane: both commit through the widget's own - submit, which a synthesised key event never reaches. `escape` abandons it — - so you can always back out and start over — but committing a rename over MCP +- **`session-rename`** (the sidebar's inline ✎ field) does not commit on + `enter`, and neither does **`tab-rename`**: both commit through the widget's + own submit, which a synthesised key event never reaches. `escape` abandons + either — so you can always back out and start over — but committing a rename + over MCP is a missing capability, tracked as [#246](https://github.com/Termherd/termherd/issues/246). - Every other overlay is exitable from the keyboard, and a test sweep derived @@ -89,18 +90,19 @@ anything applies. The error names the offender and the syntax. This is deliberate: a half-applied sequence is worse than none, because the caller cannot tell how far it got. -The action catalogue is published by the [stdio server](./stdio.md) at +The action catalogue — less the `activate-tab-N` family, which `run_action` +accepts all the same — is published by the [stdio server](./stdio.md) at `termherd://keys/schema`; the live bridge serves tools only, so its `run_action` error message carries the syntax instead. ## Example: verify a keyboard gesture end to end ```text -run_action(["split-vertical"]) → { steps: [{ outcome: "ran", +run_action(["split-vertical"]) → { steps: [{ result: "ran", action: "split-vertical" }], focused_handle: "4" } screenshot({ max_width: 900 }) → the pixels, to check the divider -press_keys(["cmd+w"]) → { steps: [{ outcome: "overlay", - overlay: "close-confirm" }] } +press_keys(["cmd+w"]) → { steps: [{ result: "overlay", + overlay: "tab-close-confirm" }] } press_keys(["escape"]) → cancelled ``` diff --git a/docs/src/mcp/live-bridge.md b/docs/src/mcp/live-bridge.md index c52398f..36bb579 100644 --- a/docs/src/mcp/live-bridge.md +++ b/docs/src/mcp/live-bridge.md @@ -16,7 +16,7 @@ session is torn down. | Tool | Args | Returns | | --- | --- | --- | | `list_sessions` | — | `{ sessions: [...] }` — each row a live session: stable `handle`, tab title, cwd, kind (`shell` / `claude`), resumed Claude id, status | -| `snapshot` | `sections`, `terminals`, `text_lines` | the whole state: config, sidebar, tabs and panes | +| `snapshot` | `sections`, `terminals`, `focused_terminal`, `text_lines` | the whole state: config, sidebar, tabs and panes | | `read_terminal` | `session`, `lines` | `{ text, rendered }` | | `screenshot` | `max_width` | the window as a PNG | @@ -25,7 +25,8 @@ schema check, which is why `list_sessions` puts its rows in a `sessions` field rather than answering the array itself. **`snapshot` is light by default**: structure only, no terminal text. Scope -text to named handles with `terminals`, or pass `sections` (any of `"config"`, +text to named handles with `terminals` (or set `focused_terminal: true` for the +focused pane, when you do not know its handle yet), or pass `sections` (any of `"config"`, `"sidebar"`, `"tabs"`) to narrow it further. `text_lines` defaults to 40. Read the structure first, then ask for a handle — that ordering is why the filter exists. diff --git a/docs/src/mcp/stdio.md b/docs/src/mcp/stdio.md index 40b34c3..06fc06a 100644 --- a/docs/src/mcp/stdio.md +++ b/docs/src/mcp/stdio.md @@ -55,7 +55,7 @@ outside `dark`/`light`) answers a JSON-RPC error and writes nothing. `null` is always accepted on a writable id — it unsets the option. That is the whole write surface today. The `close`, `sidebar`, `record`, -`open`, `keys`, `terminal.font_size`, `terminal.copy_on_select` and +`open`, `mcp`, `keys`, `terminal.font_size`, `terminal.copy_on_select` and `terminal.paste_on_right_click` blocks of [`settings.json`](../reference/settings.md) are file-only — `keys` is readable as a resource, below. @@ -71,7 +71,9 @@ readable as a resource, below. keymap itself uses, so it cannot drift from the binary you are running. It is the machine-readable form of [Keyboard shortcuts](../reference/keyboard.md), and the catalogue the live -bridge's `run_action` speaks. +bridge's `run_action` speaks — less the `activate-tab-N` family, which +`run_action` accepts but the resource does not list. `copy` and `paste` appear +with no default: theirs differ per platform and are set outside that table. ## What it is not diff --git a/docs/src/project/contributing.md b/docs/src/project/contributing.md index e491914..0dee4c4 100644 --- a/docs/src/project/contributing.md +++ b/docs/src/project/contributing.md @@ -36,9 +36,11 @@ just docs # build → docs/book/index.html just docs-serve # live reload on http://localhost:3000 ``` -`create-missing = false` in `book.toml` means a link to a page that does not -exist **fails the build** rather than silently minting an empty page. The -chapter map in `SUMMARY.md` is a promise; that setting keeps it honest. +`create-missing = false` in `book.toml` means a `SUMMARY.md` entry for a page +that does not exist **fails the build** rather than silently minting an empty +page. The chapter map is a promise; that setting keeps it honest. It covers +`SUMMARY.md` only — a broken link *inside* a page still builds, so check those +by hand. ## Three rules that surprise newcomers diff --git a/docs/src/reference/keyboard.md b/docs/src/reference/keyboard.md index 77a7805..e8d2864 100644 --- a/docs/src/reference/keyboard.md +++ b/docs/src/reference/keyboard.md @@ -92,7 +92,9 @@ way an [MCP caller](../mcp/keyboard.md) can answer a prompt it armed. ## Chord syntax Case- and order-insensitive. Modifiers `ctrl`, `shift`, `alt`, `cmd`, joined to -a key with `+`: +a key with `+`. Aliases: `control` for `ctrl`, `option` for `alt`, and +`super`, `logo`, `win` or `meta` for `cmd`. The `+` key itself is spelled +`plus`, since a literal `+` is the separator: ```json "keys": { @@ -107,7 +109,10 @@ chords are logged and skipped — they do not invalidate the rest of the file. ## Reading the live keymap -The [stdio MCP server](../mcp/stdio.md) publishes the whole catalogue — -every action with its default *and* current chords — as a resource at -`termherd://keys/schema`. It is generated from the same in-code table this page -describes, so it cannot drift from the binary you are running. +The [stdio MCP server](../mcp/stdio.md) publishes the action catalogue — each +action with its default chords and the override `settings.json` sets for it, +if any — as a resource at `termherd://keys/schema`. It is generated from the +same in-code table this page describes, so it cannot drift from the binary you +are running. Two gaps: the `activate-tab-N` family is not listed, and `copy` / +`paste` show no default, because theirs differ per platform and are set +outside that table — this page has them. diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index 001806b..34ad4b3 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -110,6 +110,17 @@ Values clamp: fps 1–60, `max_seconds` 1–600, `scale` 0.1–1.0. "record": { "fps": 8, "max_seconds": 30, "scale": 0.5 } ``` +### `mcp` + +Whether the live bridge's `prompt_in_session` may prompt another **Claude** +session — one agent driving another. Off by default; a shell session is never +gated. A single call can opt in on its own with `allow_claude_nesting: true` +(see [The live bridge](../mcp/live-bridge.md)). + +```json +"mcp": { "allow_claude_nesting": false } +``` + ### `open` The command a Cmd/Ctrl-clicked file path opens in. Omit @@ -166,9 +177,9 @@ The eight ids it covers today: `theme`, `shell.program`, `shell.args`, `terminal.colors.palette`. Two of them — `shell.program` and `shell.args` — are **read-only** over MCP: they name what TermHerd executes at the next launch, so an agent may read them but never set them. The `close`, `sidebar`, -`record`, `open`, `keys`, `terminal.font_size`, `terminal.copy_on_select` and -`terminal.paste_on_right_click` blocks are file-only for now; `keys` is -published as a read-only resource. +`record`, `open`, `mcp`, `keys`, `terminal.font_size`, +`terminal.copy_on_select` and `terminal.paste_on_right_click` blocks are +file-only for now; `keys` is published as a read-only resource. A `set_option` write lands in `settings.json` and **applies on restart**, like any other edit to the file. @@ -189,6 +200,7 @@ any other edit to the file. "sidebar": { "session_limit": 0 }, "record": { "fps": 10, "max_seconds": 20, "scale": 0.5 }, "open": { "command": "code -g {path}:{line}:{col}" }, + "mcp": { "allow_claude_nesting": false }, "keys": { "toggle-sidebar": "ctrl+alt+b", "focus-next": "ctrl+alt+right" diff --git a/docs/src/workspace/tabs-and-splits.md b/docs/src/workspace/tabs-and-splits.md index f01b88b..cb9dc28 100644 --- a/docs/src/workspace/tabs-and-splits.md +++ b/docs/src/workspace/tabs-and-splits.md @@ -53,8 +53,8 @@ there is no second, rival tab tree to drift out of sync. | Focus a neighbour | Cmd+Shift+←↑↓→ | Ctrl+Shift+←↑↓→ | A split opens a **fresh shell** beside the focused pane. Directional focus -walks the pane tree geometrically — Shift+→ goes to the -pane on the right, whatever the nesting. +walks the pane tree geometrically — Cmd/Ctrl+Shift+→ +goes to the pane on the right, whatever the nesting. Closing the **last** pane in a tab closes the tab. diff --git a/docs/src/workspace/terminal.md b/docs/src/workspace/terminal.md index 7521919..bbac232 100644 --- a/docs/src/workspace/terminal.md +++ b/docs/src/workspace/terminal.md @@ -30,11 +30,11 @@ so nothing reaches the clipboard unless you asked for it: - **`copy_on_select`** — releasing a drag, or double-clicking a word, copies the selection outright. With it off the selection still highlights and waits - for the copy chord, which reads the highlight currently on screen — so the - text you copy is the text you just selected, never what you copied last. - With nothing selected the chord does nothing — in particular in a pane - whose program owns the mouse (below), where the program's own copy of a drag - stays on the clipboard untouched. + for the copy chord, which asks the terminal for its current selection — even + one scrolled out of view — so the text you copy is the text you just + selected, never what you copied last. With nothing selected the chord does + nothing — in particular in a pane whose program owns the mouse (below), where + the program's own copy of a drag stays on the clipboard untouched. - **`paste_on_right_click`** — a right-click pastes into **the pane under the pointer**, which need not be the focused one, and is bracketed when that pane asked for bracketed paste. The click also focuses that pane, so the keys you