mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +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
@@ -122,6 +122,9 @@ import { SCREEN_BLOCKED_SOURCE } from './screen-blocked.js';
|
||||
import { PromptCacheKeepWarmService } from '../prompt-cache-keepwarm.js';
|
||||
import { composerIsEmpty, plainFrame } from '../session-submit-verifier.js';
|
||||
import { detectUsageLimitPause } from '../usage-limit-patterns.js';
|
||||
import { EditHistorySync } from './edit-history-sync.js';
|
||||
import { EditHistoryStore } from '../edit-history-store.js';
|
||||
import { probeLocalPaths, readClaudeEditSources, resolveClaudeTranscript } from '../edit-history-local-source.js';
|
||||
import { stopDeepSeekWeb } from '../deepseek-web-server.js';
|
||||
import {
|
||||
wireRespawnListeners,
|
||||
@@ -204,6 +207,7 @@ import {
|
||||
registerRebootRestoreRoutes,
|
||||
registerReadMyMindRoutes,
|
||||
registerGitStatusRoutes,
|
||||
registerEditHistoryRoutes,
|
||||
registerStatusTelemetryRoutes,
|
||||
registerSystemRoutes,
|
||||
registerCaseRoutes,
|
||||
@@ -329,6 +333,22 @@ function getOrCreateSelfSignedCert(): { key: string; cert: string } {
|
||||
export class WebServer extends EventEmitter {
|
||||
private app: FastifyInstance;
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
/**
|
||||
* The edit history panel's reader (edit-history-sync.ts). Reads only when the panel asks;
|
||||
* the main transcript is the one the transcript watcher follows (hook-reported path), or,
|
||||
* before the first hook after a restart, a lookup under the session's claude config dir.
|
||||
*/
|
||||
private readonly editHistorySync = new EditHistorySync({
|
||||
transcriptPath: (session) =>
|
||||
resolveClaudeTranscript(
|
||||
this.transcriptWatchers.get(session.id)?.getPath(),
|
||||
[session.claudeSessionId || session.id, session.id],
|
||||
session.claudeConfigDir
|
||||
),
|
||||
readSources: readClaudeEditSources,
|
||||
probe: probeLocalPaths,
|
||||
loadStore: (sessionId) => EditHistoryStore.load(sessionId),
|
||||
});
|
||||
private respawnControllers: Map<string, RespawnController> = new Map();
|
||||
private respawnTimers: Map<string, { timer: NodeJS.Timeout; endAt: number; startedAt: number }> = new Map();
|
||||
private runSummaryTrackers: Map<string, RunSummaryTracker> = new Map();
|
||||
@@ -1232,6 +1252,7 @@ export class WebServer extends EventEmitter {
|
||||
registerRebootRestoreRoutes(this.app, ctx);
|
||||
registerReadMyMindRoutes(this.app, ctx);
|
||||
registerGitStatusRoutes(this.app, ctx);
|
||||
registerEditHistoryRoutes(this.app, ctx, this.editHistorySync);
|
||||
registerStatusTelemetryRoutes(this.app, ctx);
|
||||
registerSystemRoutes(this.app, ctx);
|
||||
registerCaseRoutes(this.app, ctx);
|
||||
@@ -1555,6 +1576,13 @@ export class WebServer extends EventEmitter {
|
||||
// Stop transcript watcher
|
||||
this.stopTranscriptWatcher(sessionId);
|
||||
|
||||
// Edit history: a killed session takes its stored history (diffs can hold secrets) with it
|
||||
if (killMux) {
|
||||
void this.editHistorySync.remove(sessionId).catch((err) => {
|
||||
console.warn(`[EditHistory] Removing history of ${sessionId} failed:`, getErrorMessage(err));
|
||||
});
|
||||
}
|
||||
|
||||
// Stop and remove run summary tracker
|
||||
const summaryTracker = this.runSummaryTrackers.get(sessionId);
|
||||
if (summaryTracker) {
|
||||
@@ -4301,6 +4329,9 @@ export class WebServer extends EventEmitter {
|
||||
this.respawnTimers.clear();
|
||||
this.runSummaryTrackers.clear();
|
||||
this.transcriptWatchers.clear();
|
||||
await this.editHistorySync.flushAll().catch((err) => {
|
||||
console.warn('[EditHistory] Saving on shutdown failed:', getErrorMessage(err));
|
||||
});
|
||||
this.sessionListenerRefs.clear();
|
||||
this.scheduledRuns.clear();
|
||||
// Dispose StaleExpirationMaps (stops internal cleanup timers)
|
||||
|
||||
Reference in new issue
Block a user