mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
feat(edit-history): edit history panel for local claude sessions
Part 1 of #643 (local sessions only; remote follows as its own PR). An opt-in, per-device header button opens a panel with the active session's edits, newest first: every Edit/Write/MultiEdit/NotebookEdit of the main agent and its subagents with its diff, plus images a Bash call rendered or the agent sent. Remote, Docker and non-claude sessions answer `unsupported`. - Reads only on request: `?sync=1` when the panel opens; while it is open on that session it re-reads after an edit-capable `transcript:tool_end` or a `transcript:complete` from the existing transcript watcher. No hook, timer, second watcher or new SSE event; a wiring guard pins it. - Main transcript: the path the transcript watcher follows (hook-reported), else `<id>.jsonl` under the session's own CLAUDE_CONFIG_DIR override or the server's config dir. A config dir set outside Codeman is found with the next hook; until then the panel says so. - Store: dataPath('edit-history')/<id>.json, 0600 via tmp+rename, capped at 500 entries and 4 MiB of diff text per session, deleted with the session, blocked in isSensitivePath, never fed to /api/search. Every route goes through findSessionOrFail with req. - Images only through existing file surfaces: file-raw inside the workspace, the attachment route with notify:false outside it. - textContent rendering, bounded byte-offset reads, marker-class header button, hidden on phones (locked in the mobile policy test). Docs: Working-With-Files, Settings-Reference, architecture invariants, API reference, CLAUDE.md key pattern. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
002de32a08
commit
95e2272f89
31 files changed
+4631
-6
No files matched your search
@@ -1083,6 +1083,18 @@ Both routes accept two optional query parameters, which the UI sends from its pe
|
||||
|
||||
Lists are capped (300 files and 50 commits per repository) while the counts stay exact. A branch with no upstream reports the commits no remote has (`HEAD --not --remotes`); a repository with no remote reports `unpushedCount: 0`, since there is nothing to push to. Concurrent polls of one folder share a single git invocation; `?fresh=1` (what the panel's Refresh button and opening the panel send) skips the short-lived caches, though it still joins a computation already running.
|
||||
|
||||
## Edit history
|
||||
|
||||
What the Edit history panel reads (Settings → Header & Panels → Header, per-device, default off): the edits a **local claude session** made, main agent and subagents, read from its transcripts. Read-only. Every route resolves the session through `findSessionOrFail` (another user's session is `404`). Remote, Docker and non-claude sessions answer `state: 'unsupported'` and are never read.
|
||||
|
||||
`GET /api/sessions/:id/edits[?sync=1]` lists the stored edits, newest first, without diffs: `{ state, reason?, backfillTruncated, agents, entries }`. With `sync=1` it first reads what the transcripts gained since the last read; without it the server reads nothing. `state` is `ok`, `unavailable` (stored entries only; `reason` is `no-transcript` before the session has a transcript Codeman can find, or the read error; the transcript is the one the session's hooks report, else the newest `<id>.jsonl` under the session's `CLAUDE_CONFIG_DIR` override or the server's config dir) or `unsupported` (`reason`: `remote`, `docker`, `mode`). Each entry is `{ id, ts, path, relPath, kind: 'code'|'image', tool, created, added, removed, truncated, agentId? }`, where `tool` is the edit tool, `bash` for an image a Bash call produced or `sent` for one the agent sent; `relPath` is relative to the workspace (a subagent's worktree for its edits). `agents` maps a subagent id to `{ description, agentType, worktreeBranch? }`. `backfillTruncated: true` means older edits can still be loaded.
|
||||
|
||||
`GET /api/sessions/:id/edits/:entryId` returns one edit's diff as unified text: `{ diff, truncated }` (each hunk is capped at 64 KiB). `404` for an edit the session does not have.
|
||||
|
||||
`POST /api/sessions/:id/edits/older` reads the next older block of the transcripts and answers like the list.
|
||||
|
||||
The edits are stored per session in `~/.codeman/edit-history/<id>.json` (mode 0600, at most 500 edits and 4 MiB of diff text, the oldest dropped), deleted with the session and never fed into `/api/v1/search`. No route here serves a file: the panel shows images through `file-raw` and the attachment routes.
|
||||
|
||||
## Git worktrees of a case
|
||||
|
||||
A local case whose folder is the top of a git repository can keep **worktrees** at `<case>/.claude/worktrees/<name>`, each on its own branch, so several agents on one repository never share a checkout. This is the layout Claude Code's own `claude --worktree <name>` uses (branch `worktree-<name>`), so either tool finds the other's worktrees. Remote (SSH) and Docker cases are refused with `400 INVALID_INPUT` before git runs, and so is a local case whose folder is at or inside a Docker case workspace: `git worktree add` runs the repository's post-checkout hook and checkout filters on this machine, and a container can write there. A case folder that is not a repository, is a bare repository, or sits below the repository's top level is `400` too. The case resolves the way quick-start resolves it (a linked case is admin-only in multi-user mode, everything else is the caller's own case space; another user's case is `404`), and a case folder on a mount that does not answer is `422 OPERATION_FAILED`, never `404`.
|
||||
|
||||
@@ -722,6 +722,22 @@ Matching semantics: a query containing `/` matches the relative path, otherwise
|
||||
|
||||
⚠️ Closing the preview must **pause and unload** the media (`_stopFilePreviewMedia` in panels-ui.js): dropping the overlay's `visible` class is `display:none` and nothing else, and a DETACHED `HTMLMediaElement` keeps playing, which is how the X button used to leave a video audible with no player to pause.
|
||||
|
||||
### Edit history panel
|
||||
|
||||
**Edit history panel** (issue #643; `showEditHistory`, per-device, default OFF): which files a local claude session edited (main agent and subagents), each edit's diff, and images a Bash call produced, read from the session's transcripts by byte offset, never from git. Parser `src/edit-history-parse.ts` (pure), store `src/edit-history-store.ts`, reader `src/edit-history-local-source.ts`, sync `src/web/edit-history-sync.ts`, routes `src/web/routes/edit-history-routes.ts` (`GET /api/sessions/:id/edits[?sync=1]`, `GET …/edits/:entryId`, `POST …/edits/older`, all through `findSessionOrFail` WITH `req`), panel `edit-history-ui.js`. User guide: `docs/wiki/Working-With-Files.md` § Edit history.
|
||||
|
||||
⚠️ **Reads only on request, opt-in all the way down.** `server.ts` builds the `EditHistorySync` and hands it to the routes; its only own calls are `remove` (session deleted: `cleanupSession` with `killMux`) and `flushAll` (shutdown). No hook, no timer, no second watcher. The panel drives every read: `?sync=1` when it opens, and while it is open on that session a debounced (600 ms) re-read after a `transcript:tool_end` of an edit-capable tool (Edit, Write, MultiEdit, NotebookEdit, Bash, Agent, Task, SendUserFile) or a `transcript:complete`, both events the existing `TranscriptWatcher` already broadcasts. A closed panel reads nothing and keeps no badge. Guarded by the "reads only on request" wiring test in `test/edit-history-sync.test.ts`.
|
||||
|
||||
⚠️ **Which transcript** (`resolveClaudeTranscript`). The main transcript is the path the session's `TranscriptWatcher` follows (the hook-reported `transcript_path`), so a session is read from where its claude really writes, whatever its `CLAUDE_CONFIG_DIR`. Without a watcher (server restarted, no hook since), `findClaudeTranscript()` looks for `<claudeSessionId|id>.jsonl` under `claudeProjectsDir(session.claudeConfigDir())`: the session's own override (#255, the same dir the relaunch pin checks), else the server's. A config dir set outside Codeman (a shell profile, a wrapper) is never guessed: until the next hook such a session answers `unavailable` / `no-transcript`, and the panel says so. Subagents are `<main path without .jsonl>/subagents/agent-<id>.jsonl`. A different main file (`/clear`, resume) is read from its end with fresh cursors; the stored entries stay.
|
||||
|
||||
⚠️ **Bounded reads.** A sync is at most `MAX_FORWARD_PASSES` (3) reads, each at most `READ_BUDGET_BYTES` (8 MiB) shared by the main transcript and all subagents (main first); a first read starts that far before the end, and "Load older" reads one older block. Parsing happens in the request, line by line; nothing runs on the PTY data path. One sync per session at a time, and requests during a run fold into one more run.
|
||||
|
||||
⚠️ **The store holds secrets.** Every diff an agent wrote ends up in `dataPath('edit-history')/<sessionId>.json`, API keys and `.env` contents included. It is written 0600 via tmp + rename (directory 0700, so an existing store never keeps wider bits) and bounded per session: `MAX_ENTRIES` (500) and `MAX_STORE_BYTES` (4 MiB of diff text and paths), oldest dropped, also on load. Each hunk is capped at `MAX_HUNK_BYTES` (64 KiB), a pending Bash command at 8 KiB and agent meta strings at 1 KiB. The store is deleted with the session, blocked in `isSensitivePath` (`/.codeman*/edit-history/`, same reasoning as `intents.json`) and never fed to `/api/search`.
|
||||
|
||||
⚠️ **Images only through existing file surfaces.** A Bash image candidate counts only when the file's mtime lies in the call's window (start − 2 s … end + 2 s) and no entry already covers that file version; `SendUserFile` images need no window. The panel shows a thumbnail through the confined `file-raw` route inside the workspace, and outside it through `POST /api/sessions/:id/attachments` with `notify: false` (its extension allowlist and `isSensitivePath` apply) plus the attachment `/raw` route, one registration per path. SVGs get no thumbnail. No route of this feature serves a file, least of all a transcript path.
|
||||
|
||||
Paths, agent descriptions and diff lines render via `textContent` only (browser test with a hostile path). The header button uses the marker-class pattern (`btn-edit-history--hidden`) and stays off phones (`mobile.css`, locked in `test/mobile-header-buttons-policy.test.ts`). Remote, Docker and non-claude sessions answer `unsupported` and are never read.
|
||||
|
||||
### Config backups
|
||||
|
||||
**Config backups** (`src/config-backup.ts`, pure over the directories it is handed; `routes/config-backup-routes.ts`; the Config backups group in App Settings → System, `settings-ui.js`; contributed in #661, user guide `docs/wiki/Config-Backups.md`, endpoints in `docs/api-reference.md`): timestamped snapshots of the files that hold what a user customized, so a bad write can be undone. Settings (SYNCED): `configBackupEnabled` (absent = ON, since it protects against an event nobody schedules), `configBackupDir` (absolute, `~` expanded; empty, relative or the data dir itself = `<data dir>/backups/config`), `configBackupKeepCount` (2 to 500, default 20), `configBackupKeepDays` (0 to 3650, default 30, `0` = no age limit). A snapshot is a `cfg-YYYYMMDD-HHMMSS` folder (server local time, `-2`, `-3`… on a same-second collision; mode 0700) holding the tracked files under their own names plus a `manifest.json` (all 0600): `id`, `createdAt`, `reason` (`startup`/`auto`/`manual`/`pre-restore`), `appVersion`, the change-check `hash`, the `origin`, the `keepCount`/`keepDays` in force, and a SHA-256 and size per file. It is built in a `.tmp-cfg-*` folder and renamed into place; a pass sweeps temp folders older than an hour. Passes run at startup and every 5 minutes (`CONFIG_BACKUP_INTERVAL_MS` in `server.ts`, not in test mode), and on **Back up now** (forced).
|
||||
|
||||
@@ -61,7 +61,7 @@ supervised by systemd or launchd; npm installs report as non-updatable. See
|
||||
Chips for every optional header control, with a live preview of the resulting header:
|
||||
|
||||
Run, Font Size, System Stats, Redraw Terminal, Response Viewer, Away Digest, Session
|
||||
Manager, Attachments, File Viewer, Multi-monitor, Split, Tiles, Plan Usage, Lifecycle Log, Monitor,
|
||||
Manager, Attachments, Edit history, File Viewer, Multi-monitor, Split, Tiles, Plan Usage, Lifecycle Log, Monitor,
|
||||
Project Insights, File Browser, Subagents, Approvals Inbox, Read My Mind, Ultracode Agents,
|
||||
Ultracode Windows, Cron.
|
||||
|
||||
@@ -77,6 +77,12 @@ several projects. **Git status: git timeout** (per device, 5 to 120 seconds, def
|
||||
long one git command may run before that repository is reported as unreadable; raise it for
|
||||
repositories on a slow network share.
|
||||
|
||||
**Edit history** (per device, off by default, never on phones) adds a header button for the
|
||||
active session's edit history: every edit the agent and its subagents made, with its diff, and
|
||||
images it produced. Local Claude sessions only. Nothing is read until you open the window, and it
|
||||
stores those diffs (secrets included) under `~/.codeman/edit-history/`; see
|
||||
[Working With Files](Working-With-Files#edit-history).
|
||||
|
||||
Most default to off. The stock desktop header is system stats, File Viewer, Tiles, and the gear.
|
||||
**Header Stats Style** picks how the system stats and plan usage are drawn: *Compact*
|
||||
(default; two pills with a ring beside every value), *Tiles* (label over value with a bar underneath) or *As before* (the bars and the `5H · 7D` chip). Desktop only, per device.
|
||||
|
||||
@@ -253,6 +253,52 @@ that repository's own configuration on the host). The
|
||||
data comes from `GET /api/sessions/:id/git-status` and `GET /api/sessions/:id/git-diff`
|
||||
(see the [API reference](https://github.com/Ark0N/Codeman/blob/master/docs/api-reference.md)).
|
||||
|
||||
## Edit history
|
||||
|
||||
The Git panel shows what is different now. The edit history shows what the agent did, one edit
|
||||
at a time, including edits that were later undone or committed. Turn on **App Settings → Header &
|
||||
Panels → Header → Edit history** (per device, off by default) and a clock button appears in the
|
||||
header next to Attachments. It is not shown on phones.
|
||||
|
||||
Click it for a draggable window listing the active session's edits, newest first:
|
||||
|
||||
- One row per Edit, Write or MultiEdit, with `new` for a file the agent created, `edit` otherwise,
|
||||
and the lines added and removed. **Click a row** to see that edit's diff inline, coloured like the
|
||||
Git panel; a diff over 64 KB is cut short. **Click the path** to open the file in the preview.
|
||||
- Edits made by subagents carry a chip with the subagent's task; hover it for the agent type and
|
||||
its worktree branch. **Main agent only** hides them. **Group by file** puts the edits under
|
||||
their file, with the totals on the file's row.
|
||||
- Images an agent produced with a shell command (a render, a plot, a screenshot) appear as rows with
|
||||
a thumbnail: a `.png`, `.jpg`, `.gif`, `.webp` or `.avif` path named in the command or its output
|
||||
counts when the file's modification time falls within that command's run. Images the agent sent
|
||||
you count as well. Click one to open it in the preview.
|
||||
- **Load older** reads further back in the conversation.
|
||||
|
||||
It works for local Claude sessions only. Remote (SSH), Docker and other CLIs' sessions say so instead.
|
||||
|
||||
How it reads:
|
||||
|
||||
- **Only while you look.** The history is read from the session's Claude transcript when you open
|
||||
the window, and again after each edit while it stays open on that session. A closed window reads
|
||||
nothing, and Codeman does no work in the background for sessions nobody looks at.
|
||||
- **The transcript is found through the session's hooks.** Each prompt tells Codeman where the
|
||||
session's transcript is. Right after a server restart, before the next prompt, Codeman looks in
|
||||
the session's `CLAUDE_CONFIG_DIR` if you set one for it in Codeman, else in the default config
|
||||
directory. A `CLAUDE_CONFIG_DIR` set outside Codeman (in a shell profile or a wrapper script)
|
||||
is only known after the next prompt; until then the window says "No transcript found for this
|
||||
session yet".
|
||||
- **The history is stored, and it holds secrets.** Every diff an agent wrote is copied to
|
||||
`~/.codeman/edit-history/<session-id>.json`, including API keys or `.env` contents it edited. The file is
|
||||
readable by your user only, capped at 500 edits and 4 MiB of diff text per session (the oldest
|
||||
are dropped), never part of search, blocked from every file route, and deleted with the session.
|
||||
- **Images go through the existing file routes.** A thumbnail inside the workspace uses the File
|
||||
Viewer's route; one outside it is registered like a clicked path (no notification, same
|
||||
file-type allowlist and sensitive-file blocklist). No new route serves a file.
|
||||
|
||||
The data comes from `GET /api/sessions/:id/edits`, `GET /api/sessions/:id/edits/:entryId` and
|
||||
`POST /api/sessions/:id/edits/older` (see the
|
||||
[API reference](https://github.com/Ark0N/Codeman/blob/master/docs/api-reference.md)).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **The viewer follows the active session's workspace.** Switching tabs changes what you are
|
||||
|
||||
Reference in new issue
Block a user