Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 32 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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 <kbd>Shift</kbd>+drag takes the mouse back for a selection. The
copy chord does nothing in such a pane unless a <kbd>Shift</kbd> 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 <kbd>Shift</kbd>+drag takes the mouse back for a selection.
The copy chord does nothing in such a pane unless a <kbd>Shift</kbd> 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).
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/shell/view/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions crates/app/src/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
5 changes: 3 additions & 2 deletions docs/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions docs/settings.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 12 additions & 5 deletions docs/src/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
5 changes: 3 additions & 2 deletions docs/src/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | <kbd>Cmd</kbd>+<kbd>W</kbd> | <kbd>Ctrl</kbd>+<kbd>W</kbd> |
| Reopen the tab you just closed | <kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> |

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

Expand Down
18 changes: 10 additions & 8 deletions docs/src/mcp/keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
5 changes: 3 additions & 2 deletions docs/src/mcp/live-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions docs/src/mcp/stdio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
8 changes: 5 additions & 3 deletions docs/src/project/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 10 additions & 5 deletions docs/src/reference/keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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.
Loading
Loading