@ashx-j/lunr 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.js +2 -2
- package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.js +1 -7
- package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +21 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +1 -0
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +3 -0
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js +9 -11
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
- package/dist/catalog/models.json +1 -1
- package/dist/catalog/providers/github-copilot.json +1 -1
- package/dist/catalog/providers/google-vertex.json +1 -1
- package/dist/catalog/providers/google.json +1 -1
- package/dist/catalog/providers/nvidia.json +1 -1
- package/dist/catalog/providers/opencode-go.json +1 -1
- package/dist/catalog/providers/opencode.json +1 -1
- package/dist/catalog/providers/openrouter.json +1 -1
- package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
- package/dist/catalog/publication.json +3 -3
- package/dist/core/rollback.d.ts +1 -1
- package/dist/core/rollback.d.ts.map +1 -1
- package/dist/core/rollback.js +1 -1
- package/dist/core/rollback.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +92 -64
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/thinking-summary.d.ts +4 -4
- package/dist/modes/interactive/components/thinking-summary.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-summary.js +7 -5
- package/dist/modes/interactive/components/thinking-summary.js.map +1 -1
- package/dist/modes/interactive/components/thinking-tail.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-tail.js +5 -3
- package/dist/modes/interactive/components/thinking-tail.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +42 -40
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/providers.md +2 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/dist/core/rollback.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Captures pre-write file content before edit/write tools mutate files, keyed
|
|
5
5
|
* by user-turn and session id. `/rollback` restores the newest non-empty turn's
|
|
6
6
|
* snapshots and then rewinds the conversation via a session fork (persistent,
|
|
7
|
-
* unlike /undo).
|
|
7
|
+
* unlike /undo and /edit, which stay in the same session file).
|
|
8
8
|
*
|
|
9
9
|
* Capture modes (setting `rollbackCapture`):
|
|
10
10
|
* - copies: snapshot pre-write content; restore = write back + delete files
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../../src/core/rollback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,QAAQ;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACvB;AAsCD,8FAA8F;AAC9F,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI,CAEhG;AAwBD,wBAAgB,YAAY,CAAC,EAAE,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAOnE;AAED,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED,yFAAyF;AACzF,wBAAgB,6BAA6B,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzE;AAED,wBAAgB,wBAAwB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAEjE;AAED,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAElE;AAUD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAahE;AAED,mFAAmF;AACnF,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAyBrF;AA6BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CA2DxE;AA6ID,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yFAAyF;IACzF,aAAa,EAAE,MAAM,CAAC;CACtB;AA0CD,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,CA2DnE;AAED,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAOxG;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAwBvF;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CA6BtD","sourcesContent":["/**\n * lunR: rollback service — pre-edit file snapshots for /rollback.\n *\n * Captures pre-write file content before edit/write tools mutate files, keyed\n * by user-turn and session id. `/rollback` restores the newest non-empty turn's\n * snapshots and then rewinds the conversation via a session fork (persistent,\n * unlike /undo).\n *\n * Capture modes (setting `rollbackCapture`):\n * - copies: snapshot pre-write content; restore = write back + delete files\n * the agent created with edit/write.\n * - hybrid: copies + also delete files created OUTSIDE tools (tree scope).\n * - shadow-git: hidden git repo (deferred — behaves like copies).\n *\n * Scope (setting `rollbackScope`):\n * - tools: only edit/write tool changes.\n * - tree: also catches bash side-effects. Baseline = `git status --porcelain`\n * at turn START (pre-change content); at agent_end, files that are\n * dirty but were clean/absent at baseline are captured too (untracked\n * = created during the turn; tracked = restored from HEAD). Skipped\n * with a one-time warning when cwd is not a repo.\n *\n * Snapshots persist on disk under `~/.lunr/rollback/<session-id>/<turn-n>/`\n * (manifest.json + sha1-named .snap payloads) and are reloaded on init, so\n * rollback survives restarts. Turn granularity = one user message → agent_end.\n * Retention prunes beyond `rollbackTurns` turns.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport {\n\tcpSync,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, normalize, resolve } from \"node:path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface Snapshot {\n\texisted: boolean;\n\tcontent?: Buffer;\n\tcreatedByTool: boolean;\n}\n\ninterface TurnSnapshots {\n\tturnIndex: number;\n\tcwd?: string;\n\tfiles: Map<string, Snapshot>;\n}\n\ninterface ManifestEntry {\n\texisted: boolean;\n\tcreatedByTool: boolean;\n\tsnapFile?: string;\n}\n\ninterface TurnManifest {\n\tcwd?: string;\n\tfiles: Record<string, ManifestEntry>;\n}\n\nconst ROLLBACK_BASE = join(homedir(), \".lunr\", \"rollback\");\n\ninterface RollbackContext {\n\tsessionId: string;\n\tsettingsManager: SettingsManager | undefined;\n\tsessionForceEnabled: boolean;\n\tturns: TurnSnapshots[];\n\tcurrentTurnIndex: number;\n\trepoWarningShown: boolean;\n\texternalModWarningShown: boolean;\n\t/** Last cwd seen by beginTurn — reused by auto-started turns (mid-turn enable). */\n\tlastCwd?: string;\n}\n\nconst contexts = new Map<string, RollbackContext>();\nlet lastSessionId: string | undefined;\nlet globalSettingsManager: SettingsManager | undefined;\nlet warningHandler: ((message: string) => void) | undefined;\n\n/** Register a sink for one-time user-facing warnings (e.g. tree scope outside a git repo). */\nexport function setRollbackWarningHandler(handler: ((message: string) => void) | undefined): void {\n\twarningHandler = handler;\n}\n\nfunction createContext(sessionId: string): RollbackContext {\n\treturn {\n\t\tsessionId,\n\t\tsettingsManager: globalSettingsManager,\n\t\tsessionForceEnabled: false,\n\t\tturns: [],\n\t\tcurrentTurnIndex: -1,\n\t\trepoWarningShown: false,\n\t\texternalModWarningShown: false,\n\t};\n}\n\nfunction getContext(sessionId?: string): RollbackContext {\n\tconst sid = sessionId ?? lastSessionId ?? \"default\";\n\tlet ctx = contexts.get(sid);\n\tif (!ctx) {\n\t\tctx = createContext(sid);\n\t\tcontexts.set(sid, ctx);\n\t}\n\treturn ctx;\n}\n\nexport function initRollback(sm: SettingsManager, sid: string): void {\n\tglobalSettingsManager = sm;\n\tlastSessionId = sid;\n\tcontexts.set(sid, createContext(sid));\n\tconst ctx = contexts.get(sid)!;\n\tctx.settingsManager = sm;\n\tloadPersistedTurns(ctx);\n}\n\nexport function isRollbackEnabled(sessionId?: string): boolean {\n\tconst ctx = getContext(sessionId);\n\treturn (ctx.settingsManager?.getRollbackEnabled() ?? false) || ctx.sessionForceEnabled;\n}\n\n/** Whether rollback is force-enabled for this session (e.g. by auto permission mode). */\nexport function isRollbackSessionForceEnabled(sessionId?: string): boolean {\n\treturn getContext(sessionId).sessionForceEnabled;\n}\n\nexport function enableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = true;\n}\n\nexport function disableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = false;\n}\n\nfunction pruneTurns(ctx: RollbackContext): void {\n\tconst maxTurns = ctx.settingsManager?.getRollbackTurns() ?? 2;\n\twhile (ctx.turns.length > maxTurns) {\n\t\tconst old = ctx.turns.shift();\n\t\tif (old) cleanupTurnFiles(ctx, old);\n\t}\n}\n\n/**\n * Begin a new turn: push a new snapshot map, prune old turns beyond retention.\n * When `cwd` is given and scope is `tree`, also capture the pre-turn baseline\n * (content of all currently dirty/untracked files) so bash side-effects can be\n * restored later.\n */\nexport function beginTurn(cwd?: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: remember the session cwd so auto-started turns (mid-turn enable) can\n\t// still be constrained by the allowed-roots check.\n\tif (cwd) ctx.lastCwd = cwd;\n\tctx.currentTurnIndex++;\n\tconst turn: TurnSnapshots = { turnIndex: ctx.currentTurnIndex, cwd, files: new Map() };\n\tctx.turns.push(turn);\n\tpruneTurns(ctx);\n\tif (cwd && ctx.settingsManager?.getRollbackScope() === \"tree\") {\n\t\tcaptureTreeBaseline(ctx, cwd);\n\t}\n}\n\n/** Capture pre-write content for a file path, recording under the current turn. */\nexport function rollbackSnapshotBeforeWrite(absPath: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: auto-started turn inherits the last known cwd so restores stay\n\t// root-checked; without any recorded cwd the allowed roots still apply.\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) beginTurn(ctx.lastCwd, ctx.sessionId);\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tif (turn.files.has(absPath)) return; // already snapshotted this turn\n\n\ttry {\n\t\tconst existed = existsSync(absPath);\n\t\tlet content: Buffer | undefined;\n\t\tif (existed) {\n\t\t\tcontent = readFileSync(absPath);\n\t\t}\n\t\tturn.files.set(absPath, {\n\t\t\texisted,\n\t\t\tcontent,\n\t\t\tcreatedByTool: true,\n\t\t});\n\t\tpersistSnapshot(ctx, turn, absPath);\n\t} catch {\n\t\t// snapshot failures are non-fatal\n\t}\n}\n\n/**\n * Tree scope, turn START: snapshot the current (pre-turn) content of every\n * dirty/untracked file so modifications made during the turn can be restored.\n */\nfunction captureTreeBaseline(ctx: RollbackContext, cwd: string): void {\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { path } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue;\n\t\ttry {\n\t\t\tconst existed = existsSync(abs);\n\t\t\tturn.files.set(abs, {\n\t\t\t\texisted,\n\t\t\t\tcontent: existed ? readFileSync(abs) : undefined,\n\t\t\t\tcreatedByTool: false,\n\t\t\t});\n\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Tree scope, turn END: pick up files that became dirty during the turn but\n * were clean/absent at baseline. Untracked = created during the turn (deleted\n * on restore under hybrid); tracked-modified = restore from HEAD content.\n */\nexport function captureTreeChanges(cwd: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\tif (ctx.settingsManager?.getRollbackScope() !== \"tree\") return;\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) return;\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { status, path, source } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue; // already captured (baseline or tool snapshot)\n\t\ttry {\n\t\t\tif (status === \"??\" || status.includes(\"R\")) {\n\t\t\t\t// lunr: untracked now (or rename dest) and not in the baseline — created\n\t\t\t\t// during this turn (deleted on restore under hybrid/shadow-git).\n\t\t\t\tturn.files.set(abs, { existed: false, createdByTool: false });\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t\t// lunr: rename source existed at HEAD and is gone now — restore its\n\t\t\t\t// HEAD content on rollback, like a tracked file deleted mid-turn.\n\t\t\t\tif (status.includes(\"R\") && source) {\n\t\t\t\t\tconst sourceAbs = join(cwd, source);\n\t\t\t\t\tif (!turn.files.has(sourceAbs)) {\n\t\t\t\t\t\tconst headSrc = spawnSync(\"git\", [\"show\", `HEAD:${source}`], {\n\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (!headSrc.error && headSrc.status === 0 && headSrc.stdout) {\n\t\t\t\t\t\t\tturn.files.set(sourceAbs, {\n\t\t\t\t\t\t\t\texisted: true,\n\t\t\t\t\t\t\t\tcontent: headSrc.stdout as Buffer,\n\t\t\t\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tpersistSnapshot(ctx, turn, sourceAbs);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Tracked file that was clean at turn start; original = HEAD content.\n\t\t\t\tconst head = spawnSync(\"git\", [\"show\", `HEAD:${path}`], {\n\t\t\t\t\tcwd,\n\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t});\n\t\t\t\tif (head.error || head.status !== 0 || !head.stdout) continue;\n\t\t\t\tturn.files.set(abs, {\n\t\t\t\t\texisted: true,\n\t\t\t\t\tcontent: head.stdout as Buffer,\n\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t});\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t}\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Run `git status --porcelain -z` and parse entries, or return undefined (with\n * a one-time warning) when cwd is not a repo / git fails. Rename/copy entries\n * carry their source path in a separate bare field — parsed into `source`.\n */\nfunction gitPorcelain(\n\tctx: RollbackContext,\n\tcwd: string,\n): Array<{ status: string; path: string; source?: string }> | undefined {\n\ttry {\n\t\tconst result = spawnSync(\"git\", [\"status\", \"--porcelain\", \"-z\"], { cwd, encoding: \"utf8\", timeout: 5000 });\n\t\tif (result.error || result.status !== 0) {\n\t\t\twarnOnce(\n\t\t\t\tctx,\n\t\t\t\t\"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\",\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t\tconst fields = (result.stdout ?? \"\").split(\"\\0\").filter((f) => f.length > 0);\n\t\tconst entries: Array<{ status: string; path: string; source?: string }> = [];\n\t\tfor (let i = 0; i < fields.length; i++) {\n\t\t\tconst field = fields[i];\n\t\t\tconst status = field.slice(0, 2);\n\t\t\tconst path = field.slice(3);\n\t\t\tlet source: string | undefined;\n\t\t\tif (status.includes(\"R\") || status.includes(\"C\")) {\n\t\t\t\t// Rename/copy: the bare field after this entry is the source path.\n\t\t\t\tsource = fields[i + 1];\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tentries.push({ status, path, source });\n\t\t}\n\t\treturn entries;\n\t} catch {\n\t\twarnOnce(ctx, \"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\");\n\t\treturn undefined;\n\t}\n}\n\nfunction warnOnce(ctx: RollbackContext, message: string): void {\n\tif (ctx.repoWarningShown) return;\n\tctx.repoWarningShown = true;\n\ttry {\n\t\twarningHandler?.(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\nfunction persistSnapshot(ctx: RollbackContext, turn: TurnSnapshots, absPath: string): void {\n\ttry {\n\t\tconst snap = turn.files.get(absPath);\n\t\tif (!snap) return;\n\t\tconst turnDir = getTurnDir(ctx, turn.turnIndex);\n\t\tmkdirSync(turnDir, { recursive: true });\n\t\tconst manifestPath = join(turnDir, \"manifest.json\");\n\t\tconst manifest = readManifest(manifestPath);\n\t\tconst entry: ManifestEntry = { existed: snap.existed, createdByTool: snap.createdByTool };\n\t\tif (snap.content) {\n\t\t\tconst snapFile = snapFileName(absPath);\n\t\t\twriteFileSync(join(turnDir, snapFile), snap.content);\n\t\t\tentry.snapFile = snapFile;\n\t\t}\n\t\tmanifest.files[absPath] = entry;\n\t\tif (turn.cwd) manifest.cwd = turn.cwd;\n\t\twriteFileSync(manifestPath, JSON.stringify(manifest, null, 2));\n\t} catch {\n\t\t// disk persistence failures are non-fatal\n\t}\n}\n\nfunction readManifest(path: string): TurnManifest {\n\tif (!existsSync(path)) return { files: {} };\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\tconst files = (parsed as Record<string, unknown>).files;\n\t\t\treturn {\n\t\t\t\tcwd: typeof parsed.cwd === \"string\" ? parsed.cwd : undefined,\n\t\t\t\tfiles:\n\t\t\t\t\tfiles && typeof files === \"object\" && !Array.isArray(files)\n\t\t\t\t\t\t? (files as Record<string, ManifestEntry>)\n\t\t\t\t\t\t: {},\n\t\t\t};\n\t\t}\n\t\treturn { files: {} };\n\t} catch {\n\t\treturn { files: {} };\n\t}\n}\n\n/** Reload persisted turns from disk (called by initRollback) so rollback survives restarts. */\nfunction loadPersistedTurns(ctx: RollbackContext): void {\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\tif (!existsSync(sessionDir)) return;\n\t\tconst loaded: TurnSnapshots[] = [];\n\t\tfor (const dirName of readdirSync(sessionDir)) {\n\t\t\tconst match = /^turn-(\\d+)$/.exec(dirName);\n\t\t\tif (!match) continue;\n\t\t\tconst turnIndex = Number(match[1]);\n\t\t\tconst turnDir = join(sessionDir, dirName);\n\t\t\tconst manifest = readManifest(join(turnDir, \"manifest.json\"));\n\t\t\tconst files = new Map<string, Snapshot>();\n\t\t\tfor (const [absPath, meta] of Object.entries(manifest.files)) {\n\t\t\t\tlet content: Buffer | undefined;\n\t\t\t\tif (meta.snapFile) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcontent = readFileSync(join(turnDir, meta.snapFile));\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tcontent = undefined;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfiles.set(absPath, { existed: meta.existed, content, createdByTool: meta.createdByTool });\n\t\t\t}\n\t\t\tloaded.push({ turnIndex, cwd: manifest.cwd, files });\n\t\t}\n\t\tloaded.sort((a, b) => a.turnIndex - b.turnIndex);\n\t\tctx.turns = loaded;\n\t\tctx.currentTurnIndex = loaded.length > 0 ? loaded[loaded.length - 1].turnIndex : -1;\n\t\tpruneTurns(ctx);\n\t} catch {\n\t\t// reload failures are non-fatal — start empty\n\t}\n}\n\nfunction getTurnDir(ctx: RollbackContext, turnIndex: number): string {\n\treturn join(ROLLBACK_BASE, ctx.sessionId, `turn-${turnIndex}`);\n}\n\nfunction cleanupTurnFiles(ctx: RollbackContext, turn: TurnSnapshots): void {\n\ttry {\n\t\tconst dir = getTurnDir(ctx, turn.turnIndex);\n\t\tif (existsSync(dir)) rmSync(dir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n}\n\nexport interface RollbackResult {\n\trestored: string[];\n\tdeleted: string[];\n\t/** Number of turns consumed: the restored turn plus any empty turns skipped above it. */\n\tturnsConsumed: number;\n}\n\n/**\n * Restore the newest NON-EMPTY turn's snapshots, drop it (and any empty turns\n * on top of it), and return what actually happened. Files the agent created\n * with edit/write are deleted in every capture mode; files created outside\n * tools (tree scope) are deleted only under hybrid/shadow-git.\n */\nfunction isPathUnderRoot(absPath: string, root: string): boolean {\n\tlet normRoot = normalize(root).replace(/\\\\$/g, \"\");\n\tlet normPath = normalize(absPath).replace(/\\\\$/g, \"\");\n\t// lunr: Windows drive-letter/directory casing differs by launch context and\n\t// resolve() preserves input casing — compare case-insensitively there.\n\tif (process.platform === \"win32\") {\n\t\tnormRoot = normRoot.toLowerCase();\n\t\tnormPath = normPath.toLowerCase();\n\t}\n\treturn normPath === normRoot || normPath.startsWith(`${normRoot}/`) || normPath.startsWith(`${normRoot}\\\\`);\n}\n\nfunction isWithinAllowedRoots(absPath: string, turn: TurnSnapshots): boolean {\n\tif (!turn.cwd) return true; // no recorded cwd: cannot validate, allow (tests / legacy)\n\tconst roots = [\n\t\tROLLBACK_BASE,\n\t\tjoin(homedir(), CONFIG_DIR_NAME), // lunr: behavior.md / cron jobs.json live under the config dir\n\t\t// lunr: simple-memory lives next to the lunr agent dir (~/.lunr/simple-memory)\n\t\tjoin(dirname(getAgentDir()), \"simple-memory\"),\n\t\tresolve(turn.cwd),\n\t];\n\treturn roots.some((root) => isPathUnderRoot(absPath, root));\n}\n\nfunction warnExternalModification(ctx: RollbackContext, message: string): void {\n\tif (ctx.externalModWarningShown || !warningHandler) return;\n\tctx.externalModWarningShown = true;\n\ttry {\n\t\twarningHandler(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\n/** Count how many turns a rollback would consume (trailing empty turns + the newest non-empty one). */\nexport function peekRollbackTurnsConsumed(sessionId?: string): number {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return 0;\n\tif (ctx.turns.length === 0) return 0;\n\tlet consumed = 0;\n\tfor (let i = ctx.turns.length - 1; i >= 0; i--) {\n\t\tconsumed++;\n\t\tif (ctx.turns[i].files.size > 0) break;\n\t}\n\treturn consumed;\n}\n\nexport function rollbackLastTurn(sessionId?: string): RollbackResult {\n\tconst ctx = getContext(sessionId);\n\tconst empty: RollbackResult = { restored: [], deleted: [], turnsConsumed: 0 };\n\tif (!isRollbackEnabled(ctx.sessionId)) return empty;\n\n\tlet turnsConsumed = 0;\n\twhile (ctx.turns.length > 0 && ctx.turns[ctx.turns.length - 1].files.size === 0) {\n\t\tconst skipped = ctx.turns.pop();\n\t\tif (skipped) {\n\t\t\tcleanupTurnFiles(ctx, skipped);\n\t\t\tturnsConsumed++;\n\t\t}\n\t}\n\tif (ctx.turns.length === 0) return empty;\n\tturnsConsumed++;\n\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tconst capture = ctx.settingsManager?.getRollbackCapture() ?? \"copies\";\n\tconst restored: string[] = [];\n\tconst deleted: string[] = [];\n\n\tfor (const [absPath, snap] of turn.files) {\n\t\ttry {\n\t\t\tif (!isWithinAllowedRoots(absPath, turn)) {\n\t\t\t\twarnExternalModification(\n\t\t\t\t\tctx,\n\t\t\t\t\t`Rollback skipped ${absPath}: outside the session working directory or lunR config dir.`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (snap.existed && snap.content) {\n\t\t\t\t// For tree-scope baseline snapshots, skip the write if the file is\n\t\t\t\t// already identical to the snapshot (no actual change occurred during the\n\t\t\t\t// turn). Tool snapshots are always written so /rollback reports them.\n\t\t\t\tif (!snap.createdByTool && existsSync(absPath)) {\n\t\t\t\t\tconst current = readFileSync(absPath);\n\t\t\t\t\tif (current.equals(snap.content)) continue;\n\t\t\t\t}\n\t\t\t\tmkdirSync(dirname(absPath), { recursive: true });\n\t\t\t\twriteFileSync(absPath, snap.content);\n\t\t\t\trestored.push(absPath);\n\t\t\t} else if (!snap.existed) {\n\t\t\t\tconst shouldDelete = snap.createdByTool || capture === \"hybrid\" || capture === \"shadow-git\";\n\t\t\t\tif (shouldDelete && existsSync(absPath)) {\n\t\t\t\t\tunlinkSync(absPath);\n\t\t\t\t\tdeleted.push(absPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// existed but content unreadable — skip\n\t\t} catch {\n\t\t\t// individual file restore failures are non-fatal\n\t\t}\n\t}\n\n\tcleanupTurnFiles(ctx, turn);\n\tctx.turns.pop();\n\n\treturn { restored, deleted, turnsConsumed };\n}\n\nexport function getRollbackStatus(sessionId?: string): { enabled: boolean; turns: number; files: number } {\n\tconst ctx = getContext(sessionId);\n\treturn {\n\t\tenabled: isRollbackEnabled(ctx.sessionId),\n\t\tturns: ctx.turns.length,\n\t\tfiles: ctx.turns.reduce((sum, t) => sum + t.files.size, 0),\n\t};\n}\n\n/**\n * lunr: carry a session's rollback state across a fork — re-key the in-memory\n * context and move the on-disk dir to the forked session's id. No-op when the\n * old session has no state. Fork teardown deliberately skips clearRollback so\n * this can run right after (see interactive-mode beforeSessionInvalidate).\n */\nexport function migrateRollbackSession(oldSessionId: string, newSessionId: string): void {\n\tif (oldSessionId === newSessionId) return;\n\tconst ctx = contexts.get(oldSessionId);\n\tif (ctx) {\n\t\tcontexts.delete(oldSessionId);\n\t\tctx.sessionId = newSessionId;\n\t\tcontexts.set(newSessionId, ctx);\n\t}\n\ttry {\n\t\tconst oldDir = join(ROLLBACK_BASE, oldSessionId);\n\t\tconst newDir = join(ROLLBACK_BASE, newSessionId);\n\t\tif (existsSync(oldDir)) {\n\t\t\tif (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });\n\t\t\ttry {\n\t\t\t\trenameSync(oldDir, newDir);\n\t\t\t} catch {\n\t\t\t\t// Cross-device or locked-dir fallback: copy then delete.\n\t\t\t\tcpSync(oldDir, newDir, { recursive: true });\n\t\t\t\trmSync(oldDir, { recursive: true, force: true });\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// migration failures are non-fatal — in-memory state was still re-keyed\n\t}\n}\n\n/** Clear rollback state for a session (or all sessions when no id is passed). */\nexport function clearRollback(sessionId?: string): void {\n\tif (sessionId === undefined) {\n\t\tfor (const ctx of contexts.values()) {\n\t\t\tfor (const turn of ctx.turns) {\n\t\t\t\tcleanupTurnFiles(ctx, turn);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\t\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\tcontexts.clear();\n\t\treturn;\n\t}\n\n\tconst ctx = contexts.get(sessionId);\n\tif (!ctx) return;\n\tfor (const turn of ctx.turns) {\n\t\tcleanupTurnFiles(ctx, turn);\n\t}\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, sessionId);\n\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n\tcontexts.delete(sessionId);\n}\n\nfunction snapFileName(absPath: string): string {\n\treturn `${createHash(\"sha1\").update(absPath).digest(\"hex\")}.snap`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../../src/core/rollback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,QAAQ;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACvB;AAsCD,8FAA8F;AAC9F,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI,CAEhG;AAwBD,wBAAgB,YAAY,CAAC,EAAE,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAOnE;AAED,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED,yFAAyF;AACzF,wBAAgB,6BAA6B,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzE;AAED,wBAAgB,wBAAwB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAEjE;AAED,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAElE;AAUD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAahE;AAED,mFAAmF;AACnF,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAyBrF;AA6BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CA2DxE;AA6ID,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yFAAyF;IACzF,aAAa,EAAE,MAAM,CAAC;CACtB;AA0CD,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAUpE;AAED,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,CA2DnE;AAED,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAOxG;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAwBvF;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CA6BtD","sourcesContent":["/**\n * lunR: rollback service — pre-edit file snapshots for /rollback.\n *\n * Captures pre-write file content before edit/write tools mutate files, keyed\n * by user-turn and session id. `/rollback` restores the newest non-empty turn's\n * snapshots and then rewinds the conversation via a session fork (persistent,\n * unlike /undo and /edit, which stay in the same session file).\n *\n * Capture modes (setting `rollbackCapture`):\n * - copies: snapshot pre-write content; restore = write back + delete files\n * the agent created with edit/write.\n * - hybrid: copies + also delete files created OUTSIDE tools (tree scope).\n * - shadow-git: hidden git repo (deferred — behaves like copies).\n *\n * Scope (setting `rollbackScope`):\n * - tools: only edit/write tool changes.\n * - tree: also catches bash side-effects. Baseline = `git status --porcelain`\n * at turn START (pre-change content); at agent_end, files that are\n * dirty but were clean/absent at baseline are captured too (untracked\n * = created during the turn; tracked = restored from HEAD). Skipped\n * with a one-time warning when cwd is not a repo.\n *\n * Snapshots persist on disk under `~/.lunr/rollback/<session-id>/<turn-n>/`\n * (manifest.json + sha1-named .snap payloads) and are reloaded on init, so\n * rollback survives restarts. Turn granularity = one user message → agent_end.\n * Retention prunes beyond `rollbackTurns` turns.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport {\n\tcpSync,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, normalize, resolve } from \"node:path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface Snapshot {\n\texisted: boolean;\n\tcontent?: Buffer;\n\tcreatedByTool: boolean;\n}\n\ninterface TurnSnapshots {\n\tturnIndex: number;\n\tcwd?: string;\n\tfiles: Map<string, Snapshot>;\n}\n\ninterface ManifestEntry {\n\texisted: boolean;\n\tcreatedByTool: boolean;\n\tsnapFile?: string;\n}\n\ninterface TurnManifest {\n\tcwd?: string;\n\tfiles: Record<string, ManifestEntry>;\n}\n\nconst ROLLBACK_BASE = join(homedir(), \".lunr\", \"rollback\");\n\ninterface RollbackContext {\n\tsessionId: string;\n\tsettingsManager: SettingsManager | undefined;\n\tsessionForceEnabled: boolean;\n\tturns: TurnSnapshots[];\n\tcurrentTurnIndex: number;\n\trepoWarningShown: boolean;\n\texternalModWarningShown: boolean;\n\t/** Last cwd seen by beginTurn — reused by auto-started turns (mid-turn enable). */\n\tlastCwd?: string;\n}\n\nconst contexts = new Map<string, RollbackContext>();\nlet lastSessionId: string | undefined;\nlet globalSettingsManager: SettingsManager | undefined;\nlet warningHandler: ((message: string) => void) | undefined;\n\n/** Register a sink for one-time user-facing warnings (e.g. tree scope outside a git repo). */\nexport function setRollbackWarningHandler(handler: ((message: string) => void) | undefined): void {\n\twarningHandler = handler;\n}\n\nfunction createContext(sessionId: string): RollbackContext {\n\treturn {\n\t\tsessionId,\n\t\tsettingsManager: globalSettingsManager,\n\t\tsessionForceEnabled: false,\n\t\tturns: [],\n\t\tcurrentTurnIndex: -1,\n\t\trepoWarningShown: false,\n\t\texternalModWarningShown: false,\n\t};\n}\n\nfunction getContext(sessionId?: string): RollbackContext {\n\tconst sid = sessionId ?? lastSessionId ?? \"default\";\n\tlet ctx = contexts.get(sid);\n\tif (!ctx) {\n\t\tctx = createContext(sid);\n\t\tcontexts.set(sid, ctx);\n\t}\n\treturn ctx;\n}\n\nexport function initRollback(sm: SettingsManager, sid: string): void {\n\tglobalSettingsManager = sm;\n\tlastSessionId = sid;\n\tcontexts.set(sid, createContext(sid));\n\tconst ctx = contexts.get(sid)!;\n\tctx.settingsManager = sm;\n\tloadPersistedTurns(ctx);\n}\n\nexport function isRollbackEnabled(sessionId?: string): boolean {\n\tconst ctx = getContext(sessionId);\n\treturn (ctx.settingsManager?.getRollbackEnabled() ?? false) || ctx.sessionForceEnabled;\n}\n\n/** Whether rollback is force-enabled for this session (e.g. by auto permission mode). */\nexport function isRollbackSessionForceEnabled(sessionId?: string): boolean {\n\treturn getContext(sessionId).sessionForceEnabled;\n}\n\nexport function enableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = true;\n}\n\nexport function disableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = false;\n}\n\nfunction pruneTurns(ctx: RollbackContext): void {\n\tconst maxTurns = ctx.settingsManager?.getRollbackTurns() ?? 2;\n\twhile (ctx.turns.length > maxTurns) {\n\t\tconst old = ctx.turns.shift();\n\t\tif (old) cleanupTurnFiles(ctx, old);\n\t}\n}\n\n/**\n * Begin a new turn: push a new snapshot map, prune old turns beyond retention.\n * When `cwd` is given and scope is `tree`, also capture the pre-turn baseline\n * (content of all currently dirty/untracked files) so bash side-effects can be\n * restored later.\n */\nexport function beginTurn(cwd?: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: remember the session cwd so auto-started turns (mid-turn enable) can\n\t// still be constrained by the allowed-roots check.\n\tif (cwd) ctx.lastCwd = cwd;\n\tctx.currentTurnIndex++;\n\tconst turn: TurnSnapshots = { turnIndex: ctx.currentTurnIndex, cwd, files: new Map() };\n\tctx.turns.push(turn);\n\tpruneTurns(ctx);\n\tif (cwd && ctx.settingsManager?.getRollbackScope() === \"tree\") {\n\t\tcaptureTreeBaseline(ctx, cwd);\n\t}\n}\n\n/** Capture pre-write content for a file path, recording under the current turn. */\nexport function rollbackSnapshotBeforeWrite(absPath: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: auto-started turn inherits the last known cwd so restores stay\n\t// root-checked; without any recorded cwd the allowed roots still apply.\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) beginTurn(ctx.lastCwd, ctx.sessionId);\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tif (turn.files.has(absPath)) return; // already snapshotted this turn\n\n\ttry {\n\t\tconst existed = existsSync(absPath);\n\t\tlet content: Buffer | undefined;\n\t\tif (existed) {\n\t\t\tcontent = readFileSync(absPath);\n\t\t}\n\t\tturn.files.set(absPath, {\n\t\t\texisted,\n\t\t\tcontent,\n\t\t\tcreatedByTool: true,\n\t\t});\n\t\tpersistSnapshot(ctx, turn, absPath);\n\t} catch {\n\t\t// snapshot failures are non-fatal\n\t}\n}\n\n/**\n * Tree scope, turn START: snapshot the current (pre-turn) content of every\n * dirty/untracked file so modifications made during the turn can be restored.\n */\nfunction captureTreeBaseline(ctx: RollbackContext, cwd: string): void {\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { path } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue;\n\t\ttry {\n\t\t\tconst existed = existsSync(abs);\n\t\t\tturn.files.set(abs, {\n\t\t\t\texisted,\n\t\t\t\tcontent: existed ? readFileSync(abs) : undefined,\n\t\t\t\tcreatedByTool: false,\n\t\t\t});\n\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Tree scope, turn END: pick up files that became dirty during the turn but\n * were clean/absent at baseline. Untracked = created during the turn (deleted\n * on restore under hybrid); tracked-modified = restore from HEAD content.\n */\nexport function captureTreeChanges(cwd: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\tif (ctx.settingsManager?.getRollbackScope() !== \"tree\") return;\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) return;\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { status, path, source } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue; // already captured (baseline or tool snapshot)\n\t\ttry {\n\t\t\tif (status === \"??\" || status.includes(\"R\")) {\n\t\t\t\t// lunr: untracked now (or rename dest) and not in the baseline — created\n\t\t\t\t// during this turn (deleted on restore under hybrid/shadow-git).\n\t\t\t\tturn.files.set(abs, { existed: false, createdByTool: false });\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t\t// lunr: rename source existed at HEAD and is gone now — restore its\n\t\t\t\t// HEAD content on rollback, like a tracked file deleted mid-turn.\n\t\t\t\tif (status.includes(\"R\") && source) {\n\t\t\t\t\tconst sourceAbs = join(cwd, source);\n\t\t\t\t\tif (!turn.files.has(sourceAbs)) {\n\t\t\t\t\t\tconst headSrc = spawnSync(\"git\", [\"show\", `HEAD:${source}`], {\n\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (!headSrc.error && headSrc.status === 0 && headSrc.stdout) {\n\t\t\t\t\t\t\tturn.files.set(sourceAbs, {\n\t\t\t\t\t\t\t\texisted: true,\n\t\t\t\t\t\t\t\tcontent: headSrc.stdout as Buffer,\n\t\t\t\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tpersistSnapshot(ctx, turn, sourceAbs);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Tracked file that was clean at turn start; original = HEAD content.\n\t\t\t\tconst head = spawnSync(\"git\", [\"show\", `HEAD:${path}`], {\n\t\t\t\t\tcwd,\n\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t});\n\t\t\t\tif (head.error || head.status !== 0 || !head.stdout) continue;\n\t\t\t\tturn.files.set(abs, {\n\t\t\t\t\texisted: true,\n\t\t\t\t\tcontent: head.stdout as Buffer,\n\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t});\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t}\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Run `git status --porcelain -z` and parse entries, or return undefined (with\n * a one-time warning) when cwd is not a repo / git fails. Rename/copy entries\n * carry their source path in a separate bare field — parsed into `source`.\n */\nfunction gitPorcelain(\n\tctx: RollbackContext,\n\tcwd: string,\n): Array<{ status: string; path: string; source?: string }> | undefined {\n\ttry {\n\t\tconst result = spawnSync(\"git\", [\"status\", \"--porcelain\", \"-z\"], { cwd, encoding: \"utf8\", timeout: 5000 });\n\t\tif (result.error || result.status !== 0) {\n\t\t\twarnOnce(\n\t\t\t\tctx,\n\t\t\t\t\"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\",\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t\tconst fields = (result.stdout ?? \"\").split(\"\\0\").filter((f) => f.length > 0);\n\t\tconst entries: Array<{ status: string; path: string; source?: string }> = [];\n\t\tfor (let i = 0; i < fields.length; i++) {\n\t\t\tconst field = fields[i];\n\t\t\tconst status = field.slice(0, 2);\n\t\t\tconst path = field.slice(3);\n\t\t\tlet source: string | undefined;\n\t\t\tif (status.includes(\"R\") || status.includes(\"C\")) {\n\t\t\t\t// Rename/copy: the bare field after this entry is the source path.\n\t\t\t\tsource = fields[i + 1];\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tentries.push({ status, path, source });\n\t\t}\n\t\treturn entries;\n\t} catch {\n\t\twarnOnce(ctx, \"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\");\n\t\treturn undefined;\n\t}\n}\n\nfunction warnOnce(ctx: RollbackContext, message: string): void {\n\tif (ctx.repoWarningShown) return;\n\tctx.repoWarningShown = true;\n\ttry {\n\t\twarningHandler?.(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\nfunction persistSnapshot(ctx: RollbackContext, turn: TurnSnapshots, absPath: string): void {\n\ttry {\n\t\tconst snap = turn.files.get(absPath);\n\t\tif (!snap) return;\n\t\tconst turnDir = getTurnDir(ctx, turn.turnIndex);\n\t\tmkdirSync(turnDir, { recursive: true });\n\t\tconst manifestPath = join(turnDir, \"manifest.json\");\n\t\tconst manifest = readManifest(manifestPath);\n\t\tconst entry: ManifestEntry = { existed: snap.existed, createdByTool: snap.createdByTool };\n\t\tif (snap.content) {\n\t\t\tconst snapFile = snapFileName(absPath);\n\t\t\twriteFileSync(join(turnDir, snapFile), snap.content);\n\t\t\tentry.snapFile = snapFile;\n\t\t}\n\t\tmanifest.files[absPath] = entry;\n\t\tif (turn.cwd) manifest.cwd = turn.cwd;\n\t\twriteFileSync(manifestPath, JSON.stringify(manifest, null, 2));\n\t} catch {\n\t\t// disk persistence failures are non-fatal\n\t}\n}\n\nfunction readManifest(path: string): TurnManifest {\n\tif (!existsSync(path)) return { files: {} };\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\tconst files = (parsed as Record<string, unknown>).files;\n\t\t\treturn {\n\t\t\t\tcwd: typeof parsed.cwd === \"string\" ? parsed.cwd : undefined,\n\t\t\t\tfiles:\n\t\t\t\t\tfiles && typeof files === \"object\" && !Array.isArray(files)\n\t\t\t\t\t\t? (files as Record<string, ManifestEntry>)\n\t\t\t\t\t\t: {},\n\t\t\t};\n\t\t}\n\t\treturn { files: {} };\n\t} catch {\n\t\treturn { files: {} };\n\t}\n}\n\n/** Reload persisted turns from disk (called by initRollback) so rollback survives restarts. */\nfunction loadPersistedTurns(ctx: RollbackContext): void {\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\tif (!existsSync(sessionDir)) return;\n\t\tconst loaded: TurnSnapshots[] = [];\n\t\tfor (const dirName of readdirSync(sessionDir)) {\n\t\t\tconst match = /^turn-(\\d+)$/.exec(dirName);\n\t\t\tif (!match) continue;\n\t\t\tconst turnIndex = Number(match[1]);\n\t\t\tconst turnDir = join(sessionDir, dirName);\n\t\t\tconst manifest = readManifest(join(turnDir, \"manifest.json\"));\n\t\t\tconst files = new Map<string, Snapshot>();\n\t\t\tfor (const [absPath, meta] of Object.entries(manifest.files)) {\n\t\t\t\tlet content: Buffer | undefined;\n\t\t\t\tif (meta.snapFile) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcontent = readFileSync(join(turnDir, meta.snapFile));\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tcontent = undefined;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfiles.set(absPath, { existed: meta.existed, content, createdByTool: meta.createdByTool });\n\t\t\t}\n\t\t\tloaded.push({ turnIndex, cwd: manifest.cwd, files });\n\t\t}\n\t\tloaded.sort((a, b) => a.turnIndex - b.turnIndex);\n\t\tctx.turns = loaded;\n\t\tctx.currentTurnIndex = loaded.length > 0 ? loaded[loaded.length - 1].turnIndex : -1;\n\t\tpruneTurns(ctx);\n\t} catch {\n\t\t// reload failures are non-fatal — start empty\n\t}\n}\n\nfunction getTurnDir(ctx: RollbackContext, turnIndex: number): string {\n\treturn join(ROLLBACK_BASE, ctx.sessionId, `turn-${turnIndex}`);\n}\n\nfunction cleanupTurnFiles(ctx: RollbackContext, turn: TurnSnapshots): void {\n\ttry {\n\t\tconst dir = getTurnDir(ctx, turn.turnIndex);\n\t\tif (existsSync(dir)) rmSync(dir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n}\n\nexport interface RollbackResult {\n\trestored: string[];\n\tdeleted: string[];\n\t/** Number of turns consumed: the restored turn plus any empty turns skipped above it. */\n\tturnsConsumed: number;\n}\n\n/**\n * Restore the newest NON-EMPTY turn's snapshots, drop it (and any empty turns\n * on top of it), and return what actually happened. Files the agent created\n * with edit/write are deleted in every capture mode; files created outside\n * tools (tree scope) are deleted only under hybrid/shadow-git.\n */\nfunction isPathUnderRoot(absPath: string, root: string): boolean {\n\tlet normRoot = normalize(root).replace(/\\\\$/g, \"\");\n\tlet normPath = normalize(absPath).replace(/\\\\$/g, \"\");\n\t// lunr: Windows drive-letter/directory casing differs by launch context and\n\t// resolve() preserves input casing — compare case-insensitively there.\n\tif (process.platform === \"win32\") {\n\t\tnormRoot = normRoot.toLowerCase();\n\t\tnormPath = normPath.toLowerCase();\n\t}\n\treturn normPath === normRoot || normPath.startsWith(`${normRoot}/`) || normPath.startsWith(`${normRoot}\\\\`);\n}\n\nfunction isWithinAllowedRoots(absPath: string, turn: TurnSnapshots): boolean {\n\tif (!turn.cwd) return true; // no recorded cwd: cannot validate, allow (tests / legacy)\n\tconst roots = [\n\t\tROLLBACK_BASE,\n\t\tjoin(homedir(), CONFIG_DIR_NAME), // lunr: behavior.md / cron jobs.json live under the config dir\n\t\t// lunr: simple-memory lives next to the lunr agent dir (~/.lunr/simple-memory)\n\t\tjoin(dirname(getAgentDir()), \"simple-memory\"),\n\t\tresolve(turn.cwd),\n\t];\n\treturn roots.some((root) => isPathUnderRoot(absPath, root));\n}\n\nfunction warnExternalModification(ctx: RollbackContext, message: string): void {\n\tif (ctx.externalModWarningShown || !warningHandler) return;\n\tctx.externalModWarningShown = true;\n\ttry {\n\t\twarningHandler(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\n/** Count how many turns a rollback would consume (trailing empty turns + the newest non-empty one). */\nexport function peekRollbackTurnsConsumed(sessionId?: string): number {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return 0;\n\tif (ctx.turns.length === 0) return 0;\n\tlet consumed = 0;\n\tfor (let i = ctx.turns.length - 1; i >= 0; i--) {\n\t\tconsumed++;\n\t\tif (ctx.turns[i].files.size > 0) break;\n\t}\n\treturn consumed;\n}\n\nexport function rollbackLastTurn(sessionId?: string): RollbackResult {\n\tconst ctx = getContext(sessionId);\n\tconst empty: RollbackResult = { restored: [], deleted: [], turnsConsumed: 0 };\n\tif (!isRollbackEnabled(ctx.sessionId)) return empty;\n\n\tlet turnsConsumed = 0;\n\twhile (ctx.turns.length > 0 && ctx.turns[ctx.turns.length - 1].files.size === 0) {\n\t\tconst skipped = ctx.turns.pop();\n\t\tif (skipped) {\n\t\t\tcleanupTurnFiles(ctx, skipped);\n\t\t\tturnsConsumed++;\n\t\t}\n\t}\n\tif (ctx.turns.length === 0) return empty;\n\tturnsConsumed++;\n\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tconst capture = ctx.settingsManager?.getRollbackCapture() ?? \"copies\";\n\tconst restored: string[] = [];\n\tconst deleted: string[] = [];\n\n\tfor (const [absPath, snap] of turn.files) {\n\t\ttry {\n\t\t\tif (!isWithinAllowedRoots(absPath, turn)) {\n\t\t\t\twarnExternalModification(\n\t\t\t\t\tctx,\n\t\t\t\t\t`Rollback skipped ${absPath}: outside the session working directory or lunR config dir.`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (snap.existed && snap.content) {\n\t\t\t\t// For tree-scope baseline snapshots, skip the write if the file is\n\t\t\t\t// already identical to the snapshot (no actual change occurred during the\n\t\t\t\t// turn). Tool snapshots are always written so /rollback reports them.\n\t\t\t\tif (!snap.createdByTool && existsSync(absPath)) {\n\t\t\t\t\tconst current = readFileSync(absPath);\n\t\t\t\t\tif (current.equals(snap.content)) continue;\n\t\t\t\t}\n\t\t\t\tmkdirSync(dirname(absPath), { recursive: true });\n\t\t\t\twriteFileSync(absPath, snap.content);\n\t\t\t\trestored.push(absPath);\n\t\t\t} else if (!snap.existed) {\n\t\t\t\tconst shouldDelete = snap.createdByTool || capture === \"hybrid\" || capture === \"shadow-git\";\n\t\t\t\tif (shouldDelete && existsSync(absPath)) {\n\t\t\t\t\tunlinkSync(absPath);\n\t\t\t\t\tdeleted.push(absPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// existed but content unreadable — skip\n\t\t} catch {\n\t\t\t// individual file restore failures are non-fatal\n\t\t}\n\t}\n\n\tcleanupTurnFiles(ctx, turn);\n\tctx.turns.pop();\n\n\treturn { restored, deleted, turnsConsumed };\n}\n\nexport function getRollbackStatus(sessionId?: string): { enabled: boolean; turns: number; files: number } {\n\tconst ctx = getContext(sessionId);\n\treturn {\n\t\tenabled: isRollbackEnabled(ctx.sessionId),\n\t\tturns: ctx.turns.length,\n\t\tfiles: ctx.turns.reduce((sum, t) => sum + t.files.size, 0),\n\t};\n}\n\n/**\n * lunr: carry a session's rollback state across a fork — re-key the in-memory\n * context and move the on-disk dir to the forked session's id. No-op when the\n * old session has no state. Fork teardown deliberately skips clearRollback so\n * this can run right after (see interactive-mode beforeSessionInvalidate).\n */\nexport function migrateRollbackSession(oldSessionId: string, newSessionId: string): void {\n\tif (oldSessionId === newSessionId) return;\n\tconst ctx = contexts.get(oldSessionId);\n\tif (ctx) {\n\t\tcontexts.delete(oldSessionId);\n\t\tctx.sessionId = newSessionId;\n\t\tcontexts.set(newSessionId, ctx);\n\t}\n\ttry {\n\t\tconst oldDir = join(ROLLBACK_BASE, oldSessionId);\n\t\tconst newDir = join(ROLLBACK_BASE, newSessionId);\n\t\tif (existsSync(oldDir)) {\n\t\t\tif (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });\n\t\t\ttry {\n\t\t\t\trenameSync(oldDir, newDir);\n\t\t\t} catch {\n\t\t\t\t// Cross-device or locked-dir fallback: copy then delete.\n\t\t\t\tcpSync(oldDir, newDir, { recursive: true });\n\t\t\t\trmSync(oldDir, { recursive: true, force: true });\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// migration failures are non-fatal — in-memory state was still re-keyed\n\t}\n}\n\n/** Clear rollback state for a session (or all sessions when no id is passed). */\nexport function clearRollback(sessionId?: string): void {\n\tif (sessionId === undefined) {\n\t\tfor (const ctx of contexts.values()) {\n\t\t\tfor (const turn of ctx.turns) {\n\t\t\t\tcleanupTurnFiles(ctx, turn);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\t\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\tcontexts.clear();\n\t\treturn;\n\t}\n\n\tconst ctx = contexts.get(sessionId);\n\tif (!ctx) return;\n\tfor (const turn of ctx.turns) {\n\t\tcleanupTurnFiles(ctx, turn);\n\t}\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, sessionId);\n\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n\tcontexts.delete(sessionId);\n}\n\nfunction snapFileName(absPath: string): string {\n\treturn `${createHash(\"sha1\").update(absPath).digest(\"hex\")}.snap`;\n}\n"]}
|
package/dist/core/rollback.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Captures pre-write file content before edit/write tools mutate files, keyed
|
|
5
5
|
* by user-turn and session id. `/rollback` restores the newest non-empty turn's
|
|
6
6
|
* snapshots and then rewinds the conversation via a session fork (persistent,
|
|
7
|
-
* unlike /undo).
|
|
7
|
+
* unlike /undo and /edit, which stay in the same session file).
|
|
8
8
|
*
|
|
9
9
|
* Capture modes (setting `rollbackCapture`):
|
|
10
10
|
* - copies: snapshot pre-write content; restore = write back + delete files
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rollback.js","sourceRoot":"","sources":["../../src/core/rollback.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: rollback service — pre-edit file snapshots for /rollback.\n *\n * Captures pre-write file content before edit/write tools mutate files, keyed\n * by user-turn and session id. `/rollback` restores the newest non-empty turn's\n * snapshots and then rewinds the conversation via a session fork (persistent,\n * unlike /undo).\n *\n * Capture modes (setting `rollbackCapture`):\n * - copies: snapshot pre-write content; restore = write back + delete files\n * the agent created with edit/write.\n * - hybrid: copies + also delete files created OUTSIDE tools (tree scope).\n * - shadow-git: hidden git repo (deferred — behaves like copies).\n *\n * Scope (setting `rollbackScope`):\n * - tools: only edit/write tool changes.\n * - tree: also catches bash side-effects. Baseline = `git status --porcelain`\n * at turn START (pre-change content); at agent_end, files that are\n * dirty but were clean/absent at baseline are captured too (untracked\n * = created during the turn; tracked = restored from HEAD). Skipped\n * with a one-time warning when cwd is not a repo.\n *\n * Snapshots persist on disk under `~/.lunr/rollback/<session-id>/<turn-n>/`\n * (manifest.json + sha1-named .snap payloads) and are reloaded on init, so\n * rollback survives restarts. Turn granularity = one user message → agent_end.\n * Retention prunes beyond `rollbackTurns` turns.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport {\n\tcpSync,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, normalize, resolve } from \"node:path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface Snapshot {\n\texisted: boolean;\n\tcontent?: Buffer;\n\tcreatedByTool: boolean;\n}\n\ninterface TurnSnapshots {\n\tturnIndex: number;\n\tcwd?: string;\n\tfiles: Map<string, Snapshot>;\n}\n\ninterface ManifestEntry {\n\texisted: boolean;\n\tcreatedByTool: boolean;\n\tsnapFile?: string;\n}\n\ninterface TurnManifest {\n\tcwd?: string;\n\tfiles: Record<string, ManifestEntry>;\n}\n\nconst ROLLBACK_BASE = join(homedir(), \".lunr\", \"rollback\");\n\ninterface RollbackContext {\n\tsessionId: string;\n\tsettingsManager: SettingsManager | undefined;\n\tsessionForceEnabled: boolean;\n\tturns: TurnSnapshots[];\n\tcurrentTurnIndex: number;\n\trepoWarningShown: boolean;\n\texternalModWarningShown: boolean;\n\t/** Last cwd seen by beginTurn — reused by auto-started turns (mid-turn enable). */\n\tlastCwd?: string;\n}\n\nconst contexts = new Map<string, RollbackContext>();\nlet lastSessionId: string | undefined;\nlet globalSettingsManager: SettingsManager | undefined;\nlet warningHandler: ((message: string) => void) | undefined;\n\n/** Register a sink for one-time user-facing warnings (e.g. tree scope outside a git repo). */\nexport function setRollbackWarningHandler(handler: ((message: string) => void) | undefined): void {\n\twarningHandler = handler;\n}\n\nfunction createContext(sessionId: string): RollbackContext {\n\treturn {\n\t\tsessionId,\n\t\tsettingsManager: globalSettingsManager,\n\t\tsessionForceEnabled: false,\n\t\tturns: [],\n\t\tcurrentTurnIndex: -1,\n\t\trepoWarningShown: false,\n\t\texternalModWarningShown: false,\n\t};\n}\n\nfunction getContext(sessionId?: string): RollbackContext {\n\tconst sid = sessionId ?? lastSessionId ?? \"default\";\n\tlet ctx = contexts.get(sid);\n\tif (!ctx) {\n\t\tctx = createContext(sid);\n\t\tcontexts.set(sid, ctx);\n\t}\n\treturn ctx;\n}\n\nexport function initRollback(sm: SettingsManager, sid: string): void {\n\tglobalSettingsManager = sm;\n\tlastSessionId = sid;\n\tcontexts.set(sid, createContext(sid));\n\tconst ctx = contexts.get(sid)!;\n\tctx.settingsManager = sm;\n\tloadPersistedTurns(ctx);\n}\n\nexport function isRollbackEnabled(sessionId?: string): boolean {\n\tconst ctx = getContext(sessionId);\n\treturn (ctx.settingsManager?.getRollbackEnabled() ?? false) || ctx.sessionForceEnabled;\n}\n\n/** Whether rollback is force-enabled for this session (e.g. by auto permission mode). */\nexport function isRollbackSessionForceEnabled(sessionId?: string): boolean {\n\treturn getContext(sessionId).sessionForceEnabled;\n}\n\nexport function enableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = true;\n}\n\nexport function disableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = false;\n}\n\nfunction pruneTurns(ctx: RollbackContext): void {\n\tconst maxTurns = ctx.settingsManager?.getRollbackTurns() ?? 2;\n\twhile (ctx.turns.length > maxTurns) {\n\t\tconst old = ctx.turns.shift();\n\t\tif (old) cleanupTurnFiles(ctx, old);\n\t}\n}\n\n/**\n * Begin a new turn: push a new snapshot map, prune old turns beyond retention.\n * When `cwd` is given and scope is `tree`, also capture the pre-turn baseline\n * (content of all currently dirty/untracked files) so bash side-effects can be\n * restored later.\n */\nexport function beginTurn(cwd?: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: remember the session cwd so auto-started turns (mid-turn enable) can\n\t// still be constrained by the allowed-roots check.\n\tif (cwd) ctx.lastCwd = cwd;\n\tctx.currentTurnIndex++;\n\tconst turn: TurnSnapshots = { turnIndex: ctx.currentTurnIndex, cwd, files: new Map() };\n\tctx.turns.push(turn);\n\tpruneTurns(ctx);\n\tif (cwd && ctx.settingsManager?.getRollbackScope() === \"tree\") {\n\t\tcaptureTreeBaseline(ctx, cwd);\n\t}\n}\n\n/** Capture pre-write content for a file path, recording under the current turn. */\nexport function rollbackSnapshotBeforeWrite(absPath: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: auto-started turn inherits the last known cwd so restores stay\n\t// root-checked; without any recorded cwd the allowed roots still apply.\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) beginTurn(ctx.lastCwd, ctx.sessionId);\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tif (turn.files.has(absPath)) return; // already snapshotted this turn\n\n\ttry {\n\t\tconst existed = existsSync(absPath);\n\t\tlet content: Buffer | undefined;\n\t\tif (existed) {\n\t\t\tcontent = readFileSync(absPath);\n\t\t}\n\t\tturn.files.set(absPath, {\n\t\t\texisted,\n\t\t\tcontent,\n\t\t\tcreatedByTool: true,\n\t\t});\n\t\tpersistSnapshot(ctx, turn, absPath);\n\t} catch {\n\t\t// snapshot failures are non-fatal\n\t}\n}\n\n/**\n * Tree scope, turn START: snapshot the current (pre-turn) content of every\n * dirty/untracked file so modifications made during the turn can be restored.\n */\nfunction captureTreeBaseline(ctx: RollbackContext, cwd: string): void {\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { path } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue;\n\t\ttry {\n\t\t\tconst existed = existsSync(abs);\n\t\t\tturn.files.set(abs, {\n\t\t\t\texisted,\n\t\t\t\tcontent: existed ? readFileSync(abs) : undefined,\n\t\t\t\tcreatedByTool: false,\n\t\t\t});\n\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Tree scope, turn END: pick up files that became dirty during the turn but\n * were clean/absent at baseline. Untracked = created during the turn (deleted\n * on restore under hybrid); tracked-modified = restore from HEAD content.\n */\nexport function captureTreeChanges(cwd: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\tif (ctx.settingsManager?.getRollbackScope() !== \"tree\") return;\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) return;\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { status, path, source } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue; // already captured (baseline or tool snapshot)\n\t\ttry {\n\t\t\tif (status === \"??\" || status.includes(\"R\")) {\n\t\t\t\t// lunr: untracked now (or rename dest) and not in the baseline — created\n\t\t\t\t// during this turn (deleted on restore under hybrid/shadow-git).\n\t\t\t\tturn.files.set(abs, { existed: false, createdByTool: false });\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t\t// lunr: rename source existed at HEAD and is gone now — restore its\n\t\t\t\t// HEAD content on rollback, like a tracked file deleted mid-turn.\n\t\t\t\tif (status.includes(\"R\") && source) {\n\t\t\t\t\tconst sourceAbs = join(cwd, source);\n\t\t\t\t\tif (!turn.files.has(sourceAbs)) {\n\t\t\t\t\t\tconst headSrc = spawnSync(\"git\", [\"show\", `HEAD:${source}`], {\n\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (!headSrc.error && headSrc.status === 0 && headSrc.stdout) {\n\t\t\t\t\t\t\tturn.files.set(sourceAbs, {\n\t\t\t\t\t\t\t\texisted: true,\n\t\t\t\t\t\t\t\tcontent: headSrc.stdout as Buffer,\n\t\t\t\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tpersistSnapshot(ctx, turn, sourceAbs);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Tracked file that was clean at turn start; original = HEAD content.\n\t\t\t\tconst head = spawnSync(\"git\", [\"show\", `HEAD:${path}`], {\n\t\t\t\t\tcwd,\n\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t});\n\t\t\t\tif (head.error || head.status !== 0 || !head.stdout) continue;\n\t\t\t\tturn.files.set(abs, {\n\t\t\t\t\texisted: true,\n\t\t\t\t\tcontent: head.stdout as Buffer,\n\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t});\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t}\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Run `git status --porcelain -z` and parse entries, or return undefined (with\n * a one-time warning) when cwd is not a repo / git fails. Rename/copy entries\n * carry their source path in a separate bare field — parsed into `source`.\n */\nfunction gitPorcelain(\n\tctx: RollbackContext,\n\tcwd: string,\n): Array<{ status: string; path: string; source?: string }> | undefined {\n\ttry {\n\t\tconst result = spawnSync(\"git\", [\"status\", \"--porcelain\", \"-z\"], { cwd, encoding: \"utf8\", timeout: 5000 });\n\t\tif (result.error || result.status !== 0) {\n\t\t\twarnOnce(\n\t\t\t\tctx,\n\t\t\t\t\"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\",\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t\tconst fields = (result.stdout ?? \"\").split(\"\\0\").filter((f) => f.length > 0);\n\t\tconst entries: Array<{ status: string; path: string; source?: string }> = [];\n\t\tfor (let i = 0; i < fields.length; i++) {\n\t\t\tconst field = fields[i];\n\t\t\tconst status = field.slice(0, 2);\n\t\t\tconst path = field.slice(3);\n\t\t\tlet source: string | undefined;\n\t\t\tif (status.includes(\"R\") || status.includes(\"C\")) {\n\t\t\t\t// Rename/copy: the bare field after this entry is the source path.\n\t\t\t\tsource = fields[i + 1];\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tentries.push({ status, path, source });\n\t\t}\n\t\treturn entries;\n\t} catch {\n\t\twarnOnce(ctx, \"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\");\n\t\treturn undefined;\n\t}\n}\n\nfunction warnOnce(ctx: RollbackContext, message: string): void {\n\tif (ctx.repoWarningShown) return;\n\tctx.repoWarningShown = true;\n\ttry {\n\t\twarningHandler?.(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\nfunction persistSnapshot(ctx: RollbackContext, turn: TurnSnapshots, absPath: string): void {\n\ttry {\n\t\tconst snap = turn.files.get(absPath);\n\t\tif (!snap) return;\n\t\tconst turnDir = getTurnDir(ctx, turn.turnIndex);\n\t\tmkdirSync(turnDir, { recursive: true });\n\t\tconst manifestPath = join(turnDir, \"manifest.json\");\n\t\tconst manifest = readManifest(manifestPath);\n\t\tconst entry: ManifestEntry = { existed: snap.existed, createdByTool: snap.createdByTool };\n\t\tif (snap.content) {\n\t\t\tconst snapFile = snapFileName(absPath);\n\t\t\twriteFileSync(join(turnDir, snapFile), snap.content);\n\t\t\tentry.snapFile = snapFile;\n\t\t}\n\t\tmanifest.files[absPath] = entry;\n\t\tif (turn.cwd) manifest.cwd = turn.cwd;\n\t\twriteFileSync(manifestPath, JSON.stringify(manifest, null, 2));\n\t} catch {\n\t\t// disk persistence failures are non-fatal\n\t}\n}\n\nfunction readManifest(path: string): TurnManifest {\n\tif (!existsSync(path)) return { files: {} };\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\tconst files = (parsed as Record<string, unknown>).files;\n\t\t\treturn {\n\t\t\t\tcwd: typeof parsed.cwd === \"string\" ? parsed.cwd : undefined,\n\t\t\t\tfiles:\n\t\t\t\t\tfiles && typeof files === \"object\" && !Array.isArray(files)\n\t\t\t\t\t\t? (files as Record<string, ManifestEntry>)\n\t\t\t\t\t\t: {},\n\t\t\t};\n\t\t}\n\t\treturn { files: {} };\n\t} catch {\n\t\treturn { files: {} };\n\t}\n}\n\n/** Reload persisted turns from disk (called by initRollback) so rollback survives restarts. */\nfunction loadPersistedTurns(ctx: RollbackContext): void {\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\tif (!existsSync(sessionDir)) return;\n\t\tconst loaded: TurnSnapshots[] = [];\n\t\tfor (const dirName of readdirSync(sessionDir)) {\n\t\t\tconst match = /^turn-(\\d+)$/.exec(dirName);\n\t\t\tif (!match) continue;\n\t\t\tconst turnIndex = Number(match[1]);\n\t\t\tconst turnDir = join(sessionDir, dirName);\n\t\t\tconst manifest = readManifest(join(turnDir, \"manifest.json\"));\n\t\t\tconst files = new Map<string, Snapshot>();\n\t\t\tfor (const [absPath, meta] of Object.entries(manifest.files)) {\n\t\t\t\tlet content: Buffer | undefined;\n\t\t\t\tif (meta.snapFile) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcontent = readFileSync(join(turnDir, meta.snapFile));\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tcontent = undefined;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfiles.set(absPath, { existed: meta.existed, content, createdByTool: meta.createdByTool });\n\t\t\t}\n\t\t\tloaded.push({ turnIndex, cwd: manifest.cwd, files });\n\t\t}\n\t\tloaded.sort((a, b) => a.turnIndex - b.turnIndex);\n\t\tctx.turns = loaded;\n\t\tctx.currentTurnIndex = loaded.length > 0 ? loaded[loaded.length - 1].turnIndex : -1;\n\t\tpruneTurns(ctx);\n\t} catch {\n\t\t// reload failures are non-fatal — start empty\n\t}\n}\n\nfunction getTurnDir(ctx: RollbackContext, turnIndex: number): string {\n\treturn join(ROLLBACK_BASE, ctx.sessionId, `turn-${turnIndex}`);\n}\n\nfunction cleanupTurnFiles(ctx: RollbackContext, turn: TurnSnapshots): void {\n\ttry {\n\t\tconst dir = getTurnDir(ctx, turn.turnIndex);\n\t\tif (existsSync(dir)) rmSync(dir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n}\n\nexport interface RollbackResult {\n\trestored: string[];\n\tdeleted: string[];\n\t/** Number of turns consumed: the restored turn plus any empty turns skipped above it. */\n\tturnsConsumed: number;\n}\n\n/**\n * Restore the newest NON-EMPTY turn's snapshots, drop it (and any empty turns\n * on top of it), and return what actually happened. Files the agent created\n * with edit/write are deleted in every capture mode; files created outside\n * tools (tree scope) are deleted only under hybrid/shadow-git.\n */\nfunction isPathUnderRoot(absPath: string, root: string): boolean {\n\tlet normRoot = normalize(root).replace(/\\\\$/g, \"\");\n\tlet normPath = normalize(absPath).replace(/\\\\$/g, \"\");\n\t// lunr: Windows drive-letter/directory casing differs by launch context and\n\t// resolve() preserves input casing — compare case-insensitively there.\n\tif (process.platform === \"win32\") {\n\t\tnormRoot = normRoot.toLowerCase();\n\t\tnormPath = normPath.toLowerCase();\n\t}\n\treturn normPath === normRoot || normPath.startsWith(`${normRoot}/`) || normPath.startsWith(`${normRoot}\\\\`);\n}\n\nfunction isWithinAllowedRoots(absPath: string, turn: TurnSnapshots): boolean {\n\tif (!turn.cwd) return true; // no recorded cwd: cannot validate, allow (tests / legacy)\n\tconst roots = [\n\t\tROLLBACK_BASE,\n\t\tjoin(homedir(), CONFIG_DIR_NAME), // lunr: behavior.md / cron jobs.json live under the config dir\n\t\t// lunr: simple-memory lives next to the lunr agent dir (~/.lunr/simple-memory)\n\t\tjoin(dirname(getAgentDir()), \"simple-memory\"),\n\t\tresolve(turn.cwd),\n\t];\n\treturn roots.some((root) => isPathUnderRoot(absPath, root));\n}\n\nfunction warnExternalModification(ctx: RollbackContext, message: string): void {\n\tif (ctx.externalModWarningShown || !warningHandler) return;\n\tctx.externalModWarningShown = true;\n\ttry {\n\t\twarningHandler(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\n/** Count how many turns a rollback would consume (trailing empty turns + the newest non-empty one). */\nexport function peekRollbackTurnsConsumed(sessionId?: string): number {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return 0;\n\tif (ctx.turns.length === 0) return 0;\n\tlet consumed = 0;\n\tfor (let i = ctx.turns.length - 1; i >= 0; i--) {\n\t\tconsumed++;\n\t\tif (ctx.turns[i].files.size > 0) break;\n\t}\n\treturn consumed;\n}\n\nexport function rollbackLastTurn(sessionId?: string): RollbackResult {\n\tconst ctx = getContext(sessionId);\n\tconst empty: RollbackResult = { restored: [], deleted: [], turnsConsumed: 0 };\n\tif (!isRollbackEnabled(ctx.sessionId)) return empty;\n\n\tlet turnsConsumed = 0;\n\twhile (ctx.turns.length > 0 && ctx.turns[ctx.turns.length - 1].files.size === 0) {\n\t\tconst skipped = ctx.turns.pop();\n\t\tif (skipped) {\n\t\t\tcleanupTurnFiles(ctx, skipped);\n\t\t\tturnsConsumed++;\n\t\t}\n\t}\n\tif (ctx.turns.length === 0) return empty;\n\tturnsConsumed++;\n\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tconst capture = ctx.settingsManager?.getRollbackCapture() ?? \"copies\";\n\tconst restored: string[] = [];\n\tconst deleted: string[] = [];\n\n\tfor (const [absPath, snap] of turn.files) {\n\t\ttry {\n\t\t\tif (!isWithinAllowedRoots(absPath, turn)) {\n\t\t\t\twarnExternalModification(\n\t\t\t\t\tctx,\n\t\t\t\t\t`Rollback skipped ${absPath}: outside the session working directory or lunR config dir.`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (snap.existed && snap.content) {\n\t\t\t\t// For tree-scope baseline snapshots, skip the write if the file is\n\t\t\t\t// already identical to the snapshot (no actual change occurred during the\n\t\t\t\t// turn). Tool snapshots are always written so /rollback reports them.\n\t\t\t\tif (!snap.createdByTool && existsSync(absPath)) {\n\t\t\t\t\tconst current = readFileSync(absPath);\n\t\t\t\t\tif (current.equals(snap.content)) continue;\n\t\t\t\t}\n\t\t\t\tmkdirSync(dirname(absPath), { recursive: true });\n\t\t\t\twriteFileSync(absPath, snap.content);\n\t\t\t\trestored.push(absPath);\n\t\t\t} else if (!snap.existed) {\n\t\t\t\tconst shouldDelete = snap.createdByTool || capture === \"hybrid\" || capture === \"shadow-git\";\n\t\t\t\tif (shouldDelete && existsSync(absPath)) {\n\t\t\t\t\tunlinkSync(absPath);\n\t\t\t\t\tdeleted.push(absPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// existed but content unreadable — skip\n\t\t} catch {\n\t\t\t// individual file restore failures are non-fatal\n\t\t}\n\t}\n\n\tcleanupTurnFiles(ctx, turn);\n\tctx.turns.pop();\n\n\treturn { restored, deleted, turnsConsumed };\n}\n\nexport function getRollbackStatus(sessionId?: string): { enabled: boolean; turns: number; files: number } {\n\tconst ctx = getContext(sessionId);\n\treturn {\n\t\tenabled: isRollbackEnabled(ctx.sessionId),\n\t\tturns: ctx.turns.length,\n\t\tfiles: ctx.turns.reduce((sum, t) => sum + t.files.size, 0),\n\t};\n}\n\n/**\n * lunr: carry a session's rollback state across a fork — re-key the in-memory\n * context and move the on-disk dir to the forked session's id. No-op when the\n * old session has no state. Fork teardown deliberately skips clearRollback so\n * this can run right after (see interactive-mode beforeSessionInvalidate).\n */\nexport function migrateRollbackSession(oldSessionId: string, newSessionId: string): void {\n\tif (oldSessionId === newSessionId) return;\n\tconst ctx = contexts.get(oldSessionId);\n\tif (ctx) {\n\t\tcontexts.delete(oldSessionId);\n\t\tctx.sessionId = newSessionId;\n\t\tcontexts.set(newSessionId, ctx);\n\t}\n\ttry {\n\t\tconst oldDir = join(ROLLBACK_BASE, oldSessionId);\n\t\tconst newDir = join(ROLLBACK_BASE, newSessionId);\n\t\tif (existsSync(oldDir)) {\n\t\t\tif (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });\n\t\t\ttry {\n\t\t\t\trenameSync(oldDir, newDir);\n\t\t\t} catch {\n\t\t\t\t// Cross-device or locked-dir fallback: copy then delete.\n\t\t\t\tcpSync(oldDir, newDir, { recursive: true });\n\t\t\t\trmSync(oldDir, { recursive: true, force: true });\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// migration failures are non-fatal — in-memory state was still re-keyed\n\t}\n}\n\n/** Clear rollback state for a session (or all sessions when no id is passed). */\nexport function clearRollback(sessionId?: string): void {\n\tif (sessionId === undefined) {\n\t\tfor (const ctx of contexts.values()) {\n\t\t\tfor (const turn of ctx.turns) {\n\t\t\t\tcleanupTurnFiles(ctx, turn);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\t\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\tcontexts.clear();\n\t\treturn;\n\t}\n\n\tconst ctx = contexts.get(sessionId);\n\tif (!ctx) return;\n\tfor (const turn of ctx.turns) {\n\t\tcleanupTurnFiles(ctx, turn);\n\t}\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, sessionId);\n\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n\tcontexts.delete(sessionId);\n}\n\nfunction snapFileName(absPath: string): string {\n\treturn `${createHash(\"sha1\").update(absPath).digest(\"hex\")}.snap`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rollback.js","sourceRoot":"","sources":["../../src/core/rollback.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: rollback service — pre-edit file snapshots for /rollback.\n *\n * Captures pre-write file content before edit/write tools mutate files, keyed\n * by user-turn and session id. `/rollback` restores the newest non-empty turn's\n * snapshots and then rewinds the conversation via a session fork (persistent,\n * unlike /undo and /edit, which stay in the same session file).\n *\n * Capture modes (setting `rollbackCapture`):\n * - copies: snapshot pre-write content; restore = write back + delete files\n * the agent created with edit/write.\n * - hybrid: copies + also delete files created OUTSIDE tools (tree scope).\n * - shadow-git: hidden git repo (deferred — behaves like copies).\n *\n * Scope (setting `rollbackScope`):\n * - tools: only edit/write tool changes.\n * - tree: also catches bash side-effects. Baseline = `git status --porcelain`\n * at turn START (pre-change content); at agent_end, files that are\n * dirty but were clean/absent at baseline are captured too (untracked\n * = created during the turn; tracked = restored from HEAD). Skipped\n * with a one-time warning when cwd is not a repo.\n *\n * Snapshots persist on disk under `~/.lunr/rollback/<session-id>/<turn-n>/`\n * (manifest.json + sha1-named .snap payloads) and are reloaded on init, so\n * rollback survives restarts. Turn granularity = one user message → agent_end.\n * Retention prunes beyond `rollbackTurns` turns.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport {\n\tcpSync,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, normalize, resolve } from \"node:path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface Snapshot {\n\texisted: boolean;\n\tcontent?: Buffer;\n\tcreatedByTool: boolean;\n}\n\ninterface TurnSnapshots {\n\tturnIndex: number;\n\tcwd?: string;\n\tfiles: Map<string, Snapshot>;\n}\n\ninterface ManifestEntry {\n\texisted: boolean;\n\tcreatedByTool: boolean;\n\tsnapFile?: string;\n}\n\ninterface TurnManifest {\n\tcwd?: string;\n\tfiles: Record<string, ManifestEntry>;\n}\n\nconst ROLLBACK_BASE = join(homedir(), \".lunr\", \"rollback\");\n\ninterface RollbackContext {\n\tsessionId: string;\n\tsettingsManager: SettingsManager | undefined;\n\tsessionForceEnabled: boolean;\n\tturns: TurnSnapshots[];\n\tcurrentTurnIndex: number;\n\trepoWarningShown: boolean;\n\texternalModWarningShown: boolean;\n\t/** Last cwd seen by beginTurn — reused by auto-started turns (mid-turn enable). */\n\tlastCwd?: string;\n}\n\nconst contexts = new Map<string, RollbackContext>();\nlet lastSessionId: string | undefined;\nlet globalSettingsManager: SettingsManager | undefined;\nlet warningHandler: ((message: string) => void) | undefined;\n\n/** Register a sink for one-time user-facing warnings (e.g. tree scope outside a git repo). */\nexport function setRollbackWarningHandler(handler: ((message: string) => void) | undefined): void {\n\twarningHandler = handler;\n}\n\nfunction createContext(sessionId: string): RollbackContext {\n\treturn {\n\t\tsessionId,\n\t\tsettingsManager: globalSettingsManager,\n\t\tsessionForceEnabled: false,\n\t\tturns: [],\n\t\tcurrentTurnIndex: -1,\n\t\trepoWarningShown: false,\n\t\texternalModWarningShown: false,\n\t};\n}\n\nfunction getContext(sessionId?: string): RollbackContext {\n\tconst sid = sessionId ?? lastSessionId ?? \"default\";\n\tlet ctx = contexts.get(sid);\n\tif (!ctx) {\n\t\tctx = createContext(sid);\n\t\tcontexts.set(sid, ctx);\n\t}\n\treturn ctx;\n}\n\nexport function initRollback(sm: SettingsManager, sid: string): void {\n\tglobalSettingsManager = sm;\n\tlastSessionId = sid;\n\tcontexts.set(sid, createContext(sid));\n\tconst ctx = contexts.get(sid)!;\n\tctx.settingsManager = sm;\n\tloadPersistedTurns(ctx);\n}\n\nexport function isRollbackEnabled(sessionId?: string): boolean {\n\tconst ctx = getContext(sessionId);\n\treturn (ctx.settingsManager?.getRollbackEnabled() ?? false) || ctx.sessionForceEnabled;\n}\n\n/** Whether rollback is force-enabled for this session (e.g. by auto permission mode). */\nexport function isRollbackSessionForceEnabled(sessionId?: string): boolean {\n\treturn getContext(sessionId).sessionForceEnabled;\n}\n\nexport function enableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = true;\n}\n\nexport function disableRollbackForSession(sessionId?: string): void {\n\tgetContext(sessionId).sessionForceEnabled = false;\n}\n\nfunction pruneTurns(ctx: RollbackContext): void {\n\tconst maxTurns = ctx.settingsManager?.getRollbackTurns() ?? 2;\n\twhile (ctx.turns.length > maxTurns) {\n\t\tconst old = ctx.turns.shift();\n\t\tif (old) cleanupTurnFiles(ctx, old);\n\t}\n}\n\n/**\n * Begin a new turn: push a new snapshot map, prune old turns beyond retention.\n * When `cwd` is given and scope is `tree`, also capture the pre-turn baseline\n * (content of all currently dirty/untracked files) so bash side-effects can be\n * restored later.\n */\nexport function beginTurn(cwd?: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: remember the session cwd so auto-started turns (mid-turn enable) can\n\t// still be constrained by the allowed-roots check.\n\tif (cwd) ctx.lastCwd = cwd;\n\tctx.currentTurnIndex++;\n\tconst turn: TurnSnapshots = { turnIndex: ctx.currentTurnIndex, cwd, files: new Map() };\n\tctx.turns.push(turn);\n\tpruneTurns(ctx);\n\tif (cwd && ctx.settingsManager?.getRollbackScope() === \"tree\") {\n\t\tcaptureTreeBaseline(ctx, cwd);\n\t}\n}\n\n/** Capture pre-write content for a file path, recording under the current turn. */\nexport function rollbackSnapshotBeforeWrite(absPath: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\t// lunr: auto-started turn inherits the last known cwd so restores stay\n\t// root-checked; without any recorded cwd the allowed roots still apply.\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) beginTurn(ctx.lastCwd, ctx.sessionId);\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tif (turn.files.has(absPath)) return; // already snapshotted this turn\n\n\ttry {\n\t\tconst existed = existsSync(absPath);\n\t\tlet content: Buffer | undefined;\n\t\tif (existed) {\n\t\t\tcontent = readFileSync(absPath);\n\t\t}\n\t\tturn.files.set(absPath, {\n\t\t\texisted,\n\t\t\tcontent,\n\t\t\tcreatedByTool: true,\n\t\t});\n\t\tpersistSnapshot(ctx, turn, absPath);\n\t} catch {\n\t\t// snapshot failures are non-fatal\n\t}\n}\n\n/**\n * Tree scope, turn START: snapshot the current (pre-turn) content of every\n * dirty/untracked file so modifications made during the turn can be restored.\n */\nfunction captureTreeBaseline(ctx: RollbackContext, cwd: string): void {\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { path } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue;\n\t\ttry {\n\t\t\tconst existed = existsSync(abs);\n\t\t\tturn.files.set(abs, {\n\t\t\t\texisted,\n\t\t\t\tcontent: existed ? readFileSync(abs) : undefined,\n\t\t\t\tcreatedByTool: false,\n\t\t\t});\n\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Tree scope, turn END: pick up files that became dirty during the turn but\n * were clean/absent at baseline. Untracked = created during the turn (deleted\n * on restore under hybrid); tracked-modified = restore from HEAD content.\n */\nexport function captureTreeChanges(cwd: string, sessionId?: string): void {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return;\n\tif (ctx.settingsManager?.getRollbackScope() !== \"tree\") return;\n\tif (ctx.currentTurnIndex < 0 || ctx.turns.length === 0) return;\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tif (!turn) return;\n\n\tconst entries = gitPorcelain(ctx, cwd);\n\tif (!entries) return;\n\tfor (const { status, path, source } of entries) {\n\t\tif (!path) continue;\n\t\tconst abs = join(cwd, path);\n\t\tif (turn.files.has(abs)) continue; // already captured (baseline or tool snapshot)\n\t\ttry {\n\t\t\tif (status === \"??\" || status.includes(\"R\")) {\n\t\t\t\t// lunr: untracked now (or rename dest) and not in the baseline — created\n\t\t\t\t// during this turn (deleted on restore under hybrid/shadow-git).\n\t\t\t\tturn.files.set(abs, { existed: false, createdByTool: false });\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t\t// lunr: rename source existed at HEAD and is gone now — restore its\n\t\t\t\t// HEAD content on rollback, like a tracked file deleted mid-turn.\n\t\t\t\tif (status.includes(\"R\") && source) {\n\t\t\t\t\tconst sourceAbs = join(cwd, source);\n\t\t\t\t\tif (!turn.files.has(sourceAbs)) {\n\t\t\t\t\t\tconst headSrc = spawnSync(\"git\", [\"show\", `HEAD:${source}`], {\n\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (!headSrc.error && headSrc.status === 0 && headSrc.stdout) {\n\t\t\t\t\t\t\tturn.files.set(sourceAbs, {\n\t\t\t\t\t\t\t\texisted: true,\n\t\t\t\t\t\t\t\tcontent: headSrc.stdout as Buffer,\n\t\t\t\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tpersistSnapshot(ctx, turn, sourceAbs);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Tracked file that was clean at turn start; original = HEAD content.\n\t\t\t\tconst head = spawnSync(\"git\", [\"show\", `HEAD:${path}`], {\n\t\t\t\t\tcwd,\n\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\tmaxBuffer: 32 * 1024 * 1024,\n\t\t\t\t});\n\t\t\t\tif (head.error || head.status !== 0 || !head.stdout) continue;\n\t\t\t\tturn.files.set(abs, {\n\t\t\t\t\texisted: true,\n\t\t\t\t\tcontent: head.stdout as Buffer,\n\t\t\t\t\tcreatedByTool: false,\n\t\t\t\t});\n\t\t\t\tpersistSnapshot(ctx, turn, abs);\n\t\t\t}\n\t\t} catch {\n\t\t\t// skip unreadable\n\t\t}\n\t}\n}\n\n/**\n * Run `git status --porcelain -z` and parse entries, or return undefined (with\n * a one-time warning) when cwd is not a repo / git fails. Rename/copy entries\n * carry their source path in a separate bare field — parsed into `source`.\n */\nfunction gitPorcelain(\n\tctx: RollbackContext,\n\tcwd: string,\n): Array<{ status: string; path: string; source?: string }> | undefined {\n\ttry {\n\t\tconst result = spawnSync(\"git\", [\"status\", \"--porcelain\", \"-z\"], { cwd, encoding: \"utf8\", timeout: 5000 });\n\t\tif (result.error || result.status !== 0) {\n\t\t\twarnOnce(\n\t\t\t\tctx,\n\t\t\t\t\"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\",\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t\tconst fields = (result.stdout ?? \"\").split(\"\\0\").filter((f) => f.length > 0);\n\t\tconst entries: Array<{ status: string; path: string; source?: string }> = [];\n\t\tfor (let i = 0; i < fields.length; i++) {\n\t\t\tconst field = fields[i];\n\t\t\tconst status = field.slice(0, 2);\n\t\t\tconst path = field.slice(3);\n\t\t\tlet source: string | undefined;\n\t\t\tif (status.includes(\"R\") || status.includes(\"C\")) {\n\t\t\t\t// Rename/copy: the bare field after this entry is the source path.\n\t\t\t\tsource = fields[i + 1];\n\t\t\t\ti++;\n\t\t\t}\n\t\t\tentries.push({ status, path, source });\n\t\t}\n\t\treturn entries;\n\t} catch {\n\t\twarnOnce(ctx, \"Rollback tree scope: not a git repository (or git failed) — bash side-effects won't be captured.\");\n\t\treturn undefined;\n\t}\n}\n\nfunction warnOnce(ctx: RollbackContext, message: string): void {\n\tif (ctx.repoWarningShown) return;\n\tctx.repoWarningShown = true;\n\ttry {\n\t\twarningHandler?.(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\nfunction persistSnapshot(ctx: RollbackContext, turn: TurnSnapshots, absPath: string): void {\n\ttry {\n\t\tconst snap = turn.files.get(absPath);\n\t\tif (!snap) return;\n\t\tconst turnDir = getTurnDir(ctx, turn.turnIndex);\n\t\tmkdirSync(turnDir, { recursive: true });\n\t\tconst manifestPath = join(turnDir, \"manifest.json\");\n\t\tconst manifest = readManifest(manifestPath);\n\t\tconst entry: ManifestEntry = { existed: snap.existed, createdByTool: snap.createdByTool };\n\t\tif (snap.content) {\n\t\t\tconst snapFile = snapFileName(absPath);\n\t\t\twriteFileSync(join(turnDir, snapFile), snap.content);\n\t\t\tentry.snapFile = snapFile;\n\t\t}\n\t\tmanifest.files[absPath] = entry;\n\t\tif (turn.cwd) manifest.cwd = turn.cwd;\n\t\twriteFileSync(manifestPath, JSON.stringify(manifest, null, 2));\n\t} catch {\n\t\t// disk persistence failures are non-fatal\n\t}\n}\n\nfunction readManifest(path: string): TurnManifest {\n\tif (!existsSync(path)) return { files: {} };\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\tconst files = (parsed as Record<string, unknown>).files;\n\t\t\treturn {\n\t\t\t\tcwd: typeof parsed.cwd === \"string\" ? parsed.cwd : undefined,\n\t\t\t\tfiles:\n\t\t\t\t\tfiles && typeof files === \"object\" && !Array.isArray(files)\n\t\t\t\t\t\t? (files as Record<string, ManifestEntry>)\n\t\t\t\t\t\t: {},\n\t\t\t};\n\t\t}\n\t\treturn { files: {} };\n\t} catch {\n\t\treturn { files: {} };\n\t}\n}\n\n/** Reload persisted turns from disk (called by initRollback) so rollback survives restarts. */\nfunction loadPersistedTurns(ctx: RollbackContext): void {\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\tif (!existsSync(sessionDir)) return;\n\t\tconst loaded: TurnSnapshots[] = [];\n\t\tfor (const dirName of readdirSync(sessionDir)) {\n\t\t\tconst match = /^turn-(\\d+)$/.exec(dirName);\n\t\t\tif (!match) continue;\n\t\t\tconst turnIndex = Number(match[1]);\n\t\t\tconst turnDir = join(sessionDir, dirName);\n\t\t\tconst manifest = readManifest(join(turnDir, \"manifest.json\"));\n\t\t\tconst files = new Map<string, Snapshot>();\n\t\t\tfor (const [absPath, meta] of Object.entries(manifest.files)) {\n\t\t\t\tlet content: Buffer | undefined;\n\t\t\t\tif (meta.snapFile) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcontent = readFileSync(join(turnDir, meta.snapFile));\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tcontent = undefined;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfiles.set(absPath, { existed: meta.existed, content, createdByTool: meta.createdByTool });\n\t\t\t}\n\t\t\tloaded.push({ turnIndex, cwd: manifest.cwd, files });\n\t\t}\n\t\tloaded.sort((a, b) => a.turnIndex - b.turnIndex);\n\t\tctx.turns = loaded;\n\t\tctx.currentTurnIndex = loaded.length > 0 ? loaded[loaded.length - 1].turnIndex : -1;\n\t\tpruneTurns(ctx);\n\t} catch {\n\t\t// reload failures are non-fatal — start empty\n\t}\n}\n\nfunction getTurnDir(ctx: RollbackContext, turnIndex: number): string {\n\treturn join(ROLLBACK_BASE, ctx.sessionId, `turn-${turnIndex}`);\n}\n\nfunction cleanupTurnFiles(ctx: RollbackContext, turn: TurnSnapshots): void {\n\ttry {\n\t\tconst dir = getTurnDir(ctx, turn.turnIndex);\n\t\tif (existsSync(dir)) rmSync(dir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n}\n\nexport interface RollbackResult {\n\trestored: string[];\n\tdeleted: string[];\n\t/** Number of turns consumed: the restored turn plus any empty turns skipped above it. */\n\tturnsConsumed: number;\n}\n\n/**\n * Restore the newest NON-EMPTY turn's snapshots, drop it (and any empty turns\n * on top of it), and return what actually happened. Files the agent created\n * with edit/write are deleted in every capture mode; files created outside\n * tools (tree scope) are deleted only under hybrid/shadow-git.\n */\nfunction isPathUnderRoot(absPath: string, root: string): boolean {\n\tlet normRoot = normalize(root).replace(/\\\\$/g, \"\");\n\tlet normPath = normalize(absPath).replace(/\\\\$/g, \"\");\n\t// lunr: Windows drive-letter/directory casing differs by launch context and\n\t// resolve() preserves input casing — compare case-insensitively there.\n\tif (process.platform === \"win32\") {\n\t\tnormRoot = normRoot.toLowerCase();\n\t\tnormPath = normPath.toLowerCase();\n\t}\n\treturn normPath === normRoot || normPath.startsWith(`${normRoot}/`) || normPath.startsWith(`${normRoot}\\\\`);\n}\n\nfunction isWithinAllowedRoots(absPath: string, turn: TurnSnapshots): boolean {\n\tif (!turn.cwd) return true; // no recorded cwd: cannot validate, allow (tests / legacy)\n\tconst roots = [\n\t\tROLLBACK_BASE,\n\t\tjoin(homedir(), CONFIG_DIR_NAME), // lunr: behavior.md / cron jobs.json live under the config dir\n\t\t// lunr: simple-memory lives next to the lunr agent dir (~/.lunr/simple-memory)\n\t\tjoin(dirname(getAgentDir()), \"simple-memory\"),\n\t\tresolve(turn.cwd),\n\t];\n\treturn roots.some((root) => isPathUnderRoot(absPath, root));\n}\n\nfunction warnExternalModification(ctx: RollbackContext, message: string): void {\n\tif (ctx.externalModWarningShown || !warningHandler) return;\n\tctx.externalModWarningShown = true;\n\ttry {\n\t\twarningHandler(message);\n\t} catch {\n\t\t// warning delivery is best-effort\n\t}\n}\n\n/** Count how many turns a rollback would consume (trailing empty turns + the newest non-empty one). */\nexport function peekRollbackTurnsConsumed(sessionId?: string): number {\n\tconst ctx = getContext(sessionId);\n\tif (!isRollbackEnabled(ctx.sessionId)) return 0;\n\tif (ctx.turns.length === 0) return 0;\n\tlet consumed = 0;\n\tfor (let i = ctx.turns.length - 1; i >= 0; i--) {\n\t\tconsumed++;\n\t\tif (ctx.turns[i].files.size > 0) break;\n\t}\n\treturn consumed;\n}\n\nexport function rollbackLastTurn(sessionId?: string): RollbackResult {\n\tconst ctx = getContext(sessionId);\n\tconst empty: RollbackResult = { restored: [], deleted: [], turnsConsumed: 0 };\n\tif (!isRollbackEnabled(ctx.sessionId)) return empty;\n\n\tlet turnsConsumed = 0;\n\twhile (ctx.turns.length > 0 && ctx.turns[ctx.turns.length - 1].files.size === 0) {\n\t\tconst skipped = ctx.turns.pop();\n\t\tif (skipped) {\n\t\t\tcleanupTurnFiles(ctx, skipped);\n\t\t\tturnsConsumed++;\n\t\t}\n\t}\n\tif (ctx.turns.length === 0) return empty;\n\tturnsConsumed++;\n\n\tconst turn = ctx.turns[ctx.turns.length - 1];\n\tconst capture = ctx.settingsManager?.getRollbackCapture() ?? \"copies\";\n\tconst restored: string[] = [];\n\tconst deleted: string[] = [];\n\n\tfor (const [absPath, snap] of turn.files) {\n\t\ttry {\n\t\t\tif (!isWithinAllowedRoots(absPath, turn)) {\n\t\t\t\twarnExternalModification(\n\t\t\t\t\tctx,\n\t\t\t\t\t`Rollback skipped ${absPath}: outside the session working directory or lunR config dir.`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (snap.existed && snap.content) {\n\t\t\t\t// For tree-scope baseline snapshots, skip the write if the file is\n\t\t\t\t// already identical to the snapshot (no actual change occurred during the\n\t\t\t\t// turn). Tool snapshots are always written so /rollback reports them.\n\t\t\t\tif (!snap.createdByTool && existsSync(absPath)) {\n\t\t\t\t\tconst current = readFileSync(absPath);\n\t\t\t\t\tif (current.equals(snap.content)) continue;\n\t\t\t\t}\n\t\t\t\tmkdirSync(dirname(absPath), { recursive: true });\n\t\t\t\twriteFileSync(absPath, snap.content);\n\t\t\t\trestored.push(absPath);\n\t\t\t} else if (!snap.existed) {\n\t\t\t\tconst shouldDelete = snap.createdByTool || capture === \"hybrid\" || capture === \"shadow-git\";\n\t\t\t\tif (shouldDelete && existsSync(absPath)) {\n\t\t\t\t\tunlinkSync(absPath);\n\t\t\t\t\tdeleted.push(absPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// existed but content unreadable — skip\n\t\t} catch {\n\t\t\t// individual file restore failures are non-fatal\n\t\t}\n\t}\n\n\tcleanupTurnFiles(ctx, turn);\n\tctx.turns.pop();\n\n\treturn { restored, deleted, turnsConsumed };\n}\n\nexport function getRollbackStatus(sessionId?: string): { enabled: boolean; turns: number; files: number } {\n\tconst ctx = getContext(sessionId);\n\treturn {\n\t\tenabled: isRollbackEnabled(ctx.sessionId),\n\t\tturns: ctx.turns.length,\n\t\tfiles: ctx.turns.reduce((sum, t) => sum + t.files.size, 0),\n\t};\n}\n\n/**\n * lunr: carry a session's rollback state across a fork — re-key the in-memory\n * context and move the on-disk dir to the forked session's id. No-op when the\n * old session has no state. Fork teardown deliberately skips clearRollback so\n * this can run right after (see interactive-mode beforeSessionInvalidate).\n */\nexport function migrateRollbackSession(oldSessionId: string, newSessionId: string): void {\n\tif (oldSessionId === newSessionId) return;\n\tconst ctx = contexts.get(oldSessionId);\n\tif (ctx) {\n\t\tcontexts.delete(oldSessionId);\n\t\tctx.sessionId = newSessionId;\n\t\tcontexts.set(newSessionId, ctx);\n\t}\n\ttry {\n\t\tconst oldDir = join(ROLLBACK_BASE, oldSessionId);\n\t\tconst newDir = join(ROLLBACK_BASE, newSessionId);\n\t\tif (existsSync(oldDir)) {\n\t\t\tif (existsSync(newDir)) rmSync(newDir, { recursive: true, force: true });\n\t\t\ttry {\n\t\t\t\trenameSync(oldDir, newDir);\n\t\t\t} catch {\n\t\t\t\t// Cross-device or locked-dir fallback: copy then delete.\n\t\t\t\tcpSync(oldDir, newDir, { recursive: true });\n\t\t\t\trmSync(oldDir, { recursive: true, force: true });\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// migration failures are non-fatal — in-memory state was still re-keyed\n\t}\n}\n\n/** Clear rollback state for a session (or all sessions when no id is passed). */\nexport function clearRollback(sessionId?: string): void {\n\tif (sessionId === undefined) {\n\t\tfor (const ctx of contexts.values()) {\n\t\t\tfor (const turn of ctx.turns) {\n\t\t\t\tcleanupTurnFiles(ctx, turn);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst sessionDir = join(ROLLBACK_BASE, ctx.sessionId);\n\t\t\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t\tcontexts.clear();\n\t\treturn;\n\t}\n\n\tconst ctx = contexts.get(sessionId);\n\tif (!ctx) return;\n\tfor (const turn of ctx.turns) {\n\t\tcleanupTurnFiles(ctx, turn);\n\t}\n\ttry {\n\t\tconst sessionDir = join(ROLLBACK_BASE, sessionId);\n\t\tif (existsSync(sessionDir)) rmSync(sessionDir, { recursive: true, force: true });\n\t} catch {\n\t\t// ignore\n\t}\n\tcontexts.delete(sessionId);\n}\n\nfunction snapFileName(absPath: string): string {\n\treturn `${createHash(\"sha1\").update(absPath).digest(\"hex\")}.snap`;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,
|
|
1
|
+
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CAuDrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.ts\";\nimport type { SourceInfo } from \"./source-info.ts\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n\targumentHint?: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\", argumentHint: \"<provider/model>\" },\n\t{ name: \"refresh\", description: \"Refresh model lists for all providers\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"title\", description: \"Set session display name (alias of /name)\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"usage\", description: \"Show token totals, context, and plan usage\" },\n\t{ name: \"context\", description: \"Show estimated context window breakdown\" },\n\t{\n\t\tname: \"plan\",\n\t\tdescription: \"Switch to plan permission mode, or /plan <task> to plan a task\",\n\t\targumentHint: \"[on|off|status|<task>]\",\n\t},\n\t{\n\t\tname: \"mode\",\n\t\tdescription: \"Set permission mode: manual, yolo, plan, or auto\",\n\t\targumentHint: \"[manual|yolo|plan|auto]\",\n\t},\n\t{ name: \"manual\", description: \"Activate manual permission mode (approve every action)\" },\n\t{ name: \"yolo\", description: \"Activate YOLO permission mode (auto-approve tools)\" },\n\t{ name: \"auto\", description: \"Activate auto permission mode (fully autonomous, no questions)\" },\n\t{ name: \"processes\", description: \"View and manage background processes started this session\" },\n\t{ name: \"rollback\", description: \"Undo the last turn's file changes and rewind the conversation\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"undo\", description: \"Rewind the last turn (same session; /redo restores)\" },\n\t{ name: \"edit\", description: \"Rewind the last turn and put its text in the chat box\" },\n\t{ name: \"redo\", description: \"Restore a turn undone with /undo\" },\n\t{ name: \"trust\", description: \"Save project trust decision for future sessions\" },\n\t{ name: \"login\", description: \"Configure provider authentication\", argumentHint: \"<provider>\" },\n\t{ name: \"auth\", description: \"Configure provider authentication (alias of /login)\", argumentHint: \"<provider>\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"deauth\", description: \"Remove provider authentication (alias of /logout)\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"init\", description: \"Generate a starter AGENTS.md for this project\" },\n\t{ name: \"swarm\", description: \"Orchestrate parallel subagents for a complex task\", argumentHint: \"<task>\" },\n\t{\n\t\tname: \"research\",\n\t\tdescription: \"Deep research with cited sources\",\n\t\targumentHint: \"[--depth N] [--breadth N] <question>\",\n\t},\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"sessions\", description: \"Browse and resume sessions\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, themes, and context files\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"exit\", description: `Quit ${APP_NAME} (alias of /quit)` },\n];\n"]}
|
|
@@ -32,7 +32,8 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|
|
32
32
|
{ name: "fork", description: "Create a new fork from a previous user message" },
|
|
33
33
|
{ name: "clone", description: "Duplicate the current session at the current position" },
|
|
34
34
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
35
|
-
{ name: "undo", description: "
|
|
35
|
+
{ name: "undo", description: "Rewind the last turn (same session; /redo restores)" },
|
|
36
|
+
{ name: "edit", description: "Rewind the last turn and put its text in the chat box" },
|
|
36
37
|
{ name: "redo", description: "Restore a turn undone with /undo" },
|
|
37
38
|
{ name: "trust", description: "Save project trust decision for future sessions" },
|
|
38
39
|
{ name: "login", description: "Configure provider authentication", argumentHint: "<provider>" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAkBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE,YAAY,EAAE,kBAAkB,EAAE;IACpG,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACzE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,2CAA2C,EAAE;IAC3E,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,4CAA4C,EAAE;IAC5E,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,yCAAyC,EAAE;IAC3E;QACC,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,gEAAgE;QAC7E,YAAY,EAAE,wBAAwB;KACtC;IACD;QACC,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,kDAAkD;QAC/D,YAAY,EAAE,yBAAyB;KACvC;IACD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,wDAAwD,EAAE;IACzF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,oDAAoD,EAAE;IACnF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gEAAgE,EAAE;IAC/F,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,2DAA2D,EAAE;IAC/F,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,+DAA+D,EAAE;IAClG,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAkBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE,YAAY,EAAE,kBAAkB,EAAE;IACpG,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACzE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,2CAA2C,EAAE;IAC3E,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,4CAA4C,EAAE;IAC5E,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,yCAAyC,EAAE;IAC3E;QACC,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,gEAAgE;QAC7E,YAAY,EAAE,wBAAwB;KACtC;IACD;QACC,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,kDAAkD;QAC/D,YAAY,EAAE,yBAAyB;KACvC;IACD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,wDAAwD,EAAE;IACzF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,oDAAoD,EAAE;IACnF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gEAAgE,EAAE;IAC/F,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,2DAA2D,EAAE;IAC/F,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,+DAA+D,EAAE;IAClG,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,qDAAqD,EAAE;IACpF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACtF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,kCAAkC,EAAE;IACjE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,iDAAiD,EAAE;IACjF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE,YAAY,EAAE,YAAY,EAAE;IAC/F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,qDAAqD,EAAE,YAAY,EAAE,YAAY,EAAE;IAChH,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,mDAAmD,EAAE;IACpF,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAC9E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mDAAmD,EAAE,YAAY,EAAE,QAAQ,EAAE;IAC3G;QACC,IAAI,EAAE,UAAU;QAChB,WAAW,EAAE,kCAAkC;QAC/C,YAAY,EAAE,sCAAsC;KACpD;IACD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC/D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4EAA4E,EAAE;IAC7G,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;IACjD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,mBAAmB,EAAE;CAClE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.ts\";\nimport type { SourceInfo } from \"./source-info.ts\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n\targumentHint?: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\", argumentHint: \"<provider/model>\" },\n\t{ name: \"refresh\", description: \"Refresh model lists for all providers\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"title\", description: \"Set session display name (alias of /name)\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"usage\", description: \"Show token totals, context, and plan usage\" },\n\t{ name: \"context\", description: \"Show estimated context window breakdown\" },\n\t{\n\t\tname: \"plan\",\n\t\tdescription: \"Switch to plan permission mode, or /plan <task> to plan a task\",\n\t\targumentHint: \"[on|off|status|<task>]\",\n\t},\n\t{\n\t\tname: \"mode\",\n\t\tdescription: \"Set permission mode: manual, yolo, plan, or auto\",\n\t\targumentHint: \"[manual|yolo|plan|auto]\",\n\t},\n\t{ name: \"manual\", description: \"Activate manual permission mode (approve every action)\" },\n\t{ name: \"yolo\", description: \"Activate YOLO permission mode (auto-approve tools)\" },\n\t{ name: \"auto\", description: \"Activate auto permission mode (fully autonomous, no questions)\" },\n\t{ name: \"processes\", description: \"View and manage background processes started this session\" },\n\t{ name: \"rollback\", description: \"Undo the last turn's file changes and rewind the conversation\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"undo\", description: \"Rewind the last turn (same session; /redo restores)\" },\n\t{ name: \"edit\", description: \"Rewind the last turn and put its text in the chat box\" },\n\t{ name: \"redo\", description: \"Restore a turn undone with /undo\" },\n\t{ name: \"trust\", description: \"Save project trust decision for future sessions\" },\n\t{ name: \"login\", description: \"Configure provider authentication\", argumentHint: \"<provider>\" },\n\t{ name: \"auth\", description: \"Configure provider authentication (alias of /login)\", argumentHint: \"<provider>\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"deauth\", description: \"Remove provider authentication (alias of /logout)\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"init\", description: \"Generate a starter AGENTS.md for this project\" },\n\t{ name: \"swarm\", description: \"Orchestrate parallel subagents for a complex task\", argumentHint: \"<task>\" },\n\t{\n\t\tname: \"research\",\n\t\tdescription: \"Deep research with cited sources\",\n\t\targumentHint: \"[--depth N] [--breadth N] <question>\",\n\t},\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"sessions\", description: \"Browse and resume sessions\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, themes, and context files\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"exit\", description: `Quit ${APP_NAME} (alias of /quit)` },\n];\n"]}
|
|
@@ -15,6 +15,7 @@ export declare class AssistantMessageComponent extends Container {
|
|
|
15
15
|
private hiddenThinkingLabel;
|
|
16
16
|
private outputPad;
|
|
17
17
|
private lastMessage?;
|
|
18
|
+
private thinkingSource?;
|
|
18
19
|
private hasToolCalls;
|
|
19
20
|
private gutterRail;
|
|
20
21
|
constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, markdownTheme?: MarkdownTheme, hiddenThinkingLabel?: string, outputPad?: number, gutterRail?: boolean, thinkingCollapse?: boolean);
|
|
@@ -27,6 +28,10 @@ export declare class AssistantMessageComponent extends Container {
|
|
|
27
28
|
setHiddenThinkingLabel(label: string): void;
|
|
28
29
|
setOutputPad(padding: number): void;
|
|
29
30
|
render(width: number): string[];
|
|
30
|
-
|
|
31
|
+
private thinkingSourceOptions;
|
|
32
|
+
updateContent(message: AssistantMessage, options?: {
|
|
33
|
+
thinkingSource?: AssistantMessage;
|
|
34
|
+
}): void;
|
|
35
|
+
private renderThinkingRun;
|
|
31
36
|
}
|
|
32
37
|
//# sourceMappingURL=assistant-message.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AAE/F,OAAO,EAGN,KAAK,iBAAiB,EAEtB,MAAM,uBAAuB,CAAC;AAO/B;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,UAAU,UAAQ;IAClB,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IAGnC,OAAO,CAAC,gBAAgB,CAAU;IAGlC,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,eAAe,CAAC,CAAsB;IAC9C,OAAO,CAAC,aAAa,CAAgB;IAGrC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,UAAU,CAAU;IAE5B,YACC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,mBAAmB,SAAgB,EACnC,SAAS,SAAI,EACb,UAAU,UAAQ,EAClB,gBAAgB,UAAQ,EAkBxB;IAEQ,UAAU,IAAI,IAAI,CAK1B;IAED,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAKxC;IAGD,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAK3C;IAID,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAED,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,SAAS,GAAG,IAAI,CAEjE;IAGD,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEpC;IAED,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAK1C;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKlC;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA6BvC;IAED,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAuJ7C;CACD","sourcesContent":["import type { AssistantMessage } from \"@ashx-j/lunr-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport {\n\tformatThoughtDuration,\n\tisThinkingRunComplete,\n\ttype ThinkingRunTiming,\n\tthinkingSnippet,\n} from \"./thinking-summary.ts\";\nimport { ThinkingTailComponent } from \"./thinking-tail.ts\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tselectable = true;\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\t// lunr: collapsible reasoning — when true, a completed thinking run renders as\n\t// \"✻ Thought for Xs\" + its first sentence instead of the full block.\n\tprivate thinkingCollapse: boolean;\n\t// lunr: ctrl+o expansion — when true, collapsed thinking runs render in full\n\t// (wired into the global app.tools.expand toggle via the Expandable interface).\n\tprivate expanded = false;\n\t// lunr: live per-run timings from interactive-mode; undefined = history message\n\t// (always treated as final, rendered without durations).\n\tprivate thinkingTimings?: ThinkingRunTiming[];\n\tprivate markdownTheme: MarkdownTheme;\n\t// lunr: kept for the deferred alternative hidden-thinking indicator (Phase 8 removed the label render).\n\t// biome-ignore lint/correctness/noUnusedPrivateClassMembers: retained for future use\n\tprivate hiddenThinkingLabel: string;\n\tprivate outputPad: number;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\t// lunr: gutter rail — prefix rendered lines with a dim │; the last line uses ╰\n\t// when this component closes the turn (no tool calls follow).\n\tprivate gutterRail: boolean;\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t\toutputPad = 1,\n\t\tgutterRail = false,\n\t\tthinkingCollapse = false,\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.thinkingCollapse = thinkingCollapse;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\t\tthis.outputPad = outputPad;\n\t\tthis.gutterRail = gutterRail;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t// lunr: collapsible reasoning toggles (live; re-render like setHideThinkingBlock).\n\tsetThinkingCollapse(collapse: boolean): void {\n\t\tthis.thinkingCollapse = collapse;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t// lunr: ctrl+o expansion — Expandable interface hook; expanded collapsed thinking\n\t// runs render as full Markdown blocks again.\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetThinkingTimings(timings: ThinkingRunTiming[] | undefined): void {\n\t\tthis.thinkingTimings = timings;\n\t}\n\n\t// lunr: gutter rail toggle (live; applied on next render).\n\tsetGutterRail(enabled: boolean): void {\n\t\tthis.gutterRail = enabled;\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetOutputPad(padding: number): void {\n\t\tthis.outputPad = padding;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\toverride render(width: number): string[] {\n\t\t// lunr: gutter rail — render content at width-2 and prefix each line with a\n\t\t// dim │; the last line uses ╰ when this assistant message closes the turn\n\t\t// (no tool calls follow). When tool calls are present the rail stays open (│)\n\t\t// because tool-execution components render their own borders below.\n\t\tconst railEnabled = this.gutterRail;\n\t\tconst contentWidth = railEnabled ? Math.max(1, width - 2) : width;\n\t\tconst lines = super.render(contentWidth);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\tif (railEnabled && lines.length > 0) {\n\t\t\t\tconst rail = theme.fg(\"dim\", \"│ \");\n\t\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\t\tlines[i] = rail + lines[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tlines[0] = OSC133_ZONE_START + lines[0];\n\t\tlines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];\n\n\t\tif (railEnabled) {\n\t\t\tconst rail = theme.fg(\"dim\", \"│ \");\n\t\t\tconst close = theme.fg(\"dim\", \"╰ \");\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tlines[i] = (i === lines.length - 1 ? close : rail) + lines[i];\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n\n\tupdateContent(message: AssistantMessage): void {\n\t\tthis.lastMessage = message;\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tlet isFirstTextBlock = true;\n\t\tlet thinkingRunIndex = -1;\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\t// lunr: leading ● marks the first text block (never thinking blocks). It rides\n\t\t\t\t// the defaultTextStyle below, so it renders bright white like the body.\n\t\t\t\tconst trimmed = content.text.trim();\n\t\t\t\tconst text = isFirstTextBlock ? `● ${trimmed}` : trimmed;\n\t\t\t\tisFirstTextBlock = false;\n\t\t\t\t// lunr: theme-polish — assistant message body renders bright white via the\n\t\t\t\t// shared userMessageText token (same knob as user messages).\n\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\tnew Markdown(text, this.outputPad, 0, this.markdownTheme, {\n\t\t\t\t\t\tcolor: (body: string) => theme.fg(\"userMessageText\", body),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} else if (content.type === \"thinking\") {\n\t\t\t\tthinkingRunIndex++;\n\t\t\t\tconst thinkingBlocks: string[] = [];\n\t\t\t\tfor (; i < message.content.length; i++) {\n\t\t\t\t\tconst thinkingContent = message.content[i];\n\t\t\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\t\t\tif (thinking) {\n\t\t\t\t\t\tthinkingBlocks.push(thinking);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti--;\n\n\t\t\t\tif (thinkingBlocks.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// lunr: thinking blocks are hidden — render nothing (the \"Thinking...\" label is\n\t\t\t\t\t// removed; an alternative hidden indicator will be designed separately).\n\t\t\t\t} else {\n\t\t\t\t\t// lunr: a run is complete when a block follows it, its timing closed, or no\n\t\t\t\t\t// timings are attached (history). Incomplete = still streaming.\n\t\t\t\t\tconst runComplete = isThinkingRunComplete(\n\t\t\t\t\t\ti < message.content.length - 1,\n\t\t\t\t\t\tthis.thinkingTimings?.[thinkingRunIndex],\n\t\t\t\t\t\tthis.thinkingTimings !== undefined,\n\t\t\t\t\t);\n\t\t\t\t\tif (this.thinkingCollapse && !this.expanded && runComplete) {\n\t\t\t\t\t\t// lunr: collapsible reasoning — a completed run collapses to\n\t\t\t\t\t\t// \"✻ Thought for Xs\" (duration only from live timings) + its first sentence.\n\t\t\t\t\t\tconst timing = this.thinkingTimings?.[thinkingRunIndex];\n\t\t\t\t\t\tconst label =\n\t\t\t\t\t\t\ttiming?.end !== undefined\n\t\t\t\t\t\t\t\t? `✻ Thought for ${formatThoughtDuration(timing.end - timing.start)}`\n\t\t\t\t\t\t\t\t: \"✻ Thought\";\n\t\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"thinkingText\", theme.italic(label)), this.outputPad, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst snippet = thinkingSnippet(thinkingBlocks.join(\"\\n\\n\"));\n\t\t\t\t\t\tif (snippet) {\n\t\t\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\t\t\tnew Text(theme.fg(\"thinkingText\", theme.italic(snippet)), this.outputPad + 2, 0),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (!runComplete && !this.expanded) {\n\t\t\t\t\t\t// lunr: rolling window — a still-streaming run shows only its last\n\t\t\t\t\t\t// THINKING_TAIL_LINES rendered lines; older lines disappear as new\n\t\t\t\t\t\t// ones stream in. ctrl+o expand (or completion) renders in full.\n\t\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\t\tnew ThinkingTailComponent(thinkingBlocks.join(\"\\n\\n\"), this.outputPad, 0, this.markdownTheme, {\n\t\t\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\t\t\titalic: true,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Render each run of thinking blocks as one Markdown section.\n\t\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\t\tnew Markdown(thinkingBlocks.join(\"\\n\\n\"), this.outputPad, 0, this.markdownTheme, {\n\t\t\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\t\t\titalic: true,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t\t// lunr: only add the spacer when thinking is visible (avoids stray blank line when hidden).\n\t\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if incomplete/failed - show after partial content.\n\t\t// For aborted/error tool calls, tool execution components show the error.\n\t\t// Length stops can happen before a tool call is complete, so surface them here too.\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t\tif (message.stopReason === \"length\") {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.\",\n\t\t\t\t\t),\n\t\t\t\t\tthis.outputPad,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t} else if (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), this.outputPad, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), this.outputPad, 0));\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AAE/F,OAAO,EAGN,KAAK,iBAAiB,EAEtB,MAAM,uBAAuB,CAAC;AAO/B;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,UAAU,UAAQ;IAClB,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IAGnC,OAAO,CAAC,gBAAgB,CAAU;IAGlC,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,eAAe,CAAC,CAAsB;IAC9C,OAAO,CAAC,aAAa,CAAgB;IAGrC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,cAAc,CAAC,CAAmB;IAC1C,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,UAAU,CAAU;IAE5B,YACC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,mBAAmB,SAAgB,EACnC,SAAS,SAAI,EACb,UAAU,UAAQ,EAClB,gBAAgB,UAAQ,EAkBxB;IAEQ,UAAU,IAAI,IAAI,CAK1B;IAED,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAKxC;IAGD,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAK3C;IAID,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAED,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,SAAS,GAAG,IAAI,CAEjE;IAGD,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEpC;IAED,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAK1C;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKlC;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA6BvC;IAED,OAAO,CAAC,qBAAqB;IAI7B,aAAa,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,gBAAgB,CAAA;KAAE,GAAG,IAAI,CAwG9F;IAED,OAAO,CAAC,iBAAiB;CA0DzB","sourcesContent":["import type { AssistantMessage } from \"@ashx-j/lunr-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport {\n\tformatThoughtDuration,\n\tisThinkingRunComplete,\n\ttype ThinkingRunTiming,\n\tthinkingSnippet,\n} from \"./thinking-summary.ts\";\nimport { ThinkingTailComponent } from \"./thinking-tail.ts\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tselectable = true;\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\t// lunr: collapsible reasoning — when true, a completed thinking run renders as\n\t// \"✻ Thought for Xs\" + its first sentence instead of the full block.\n\tprivate thinkingCollapse: boolean;\n\t// lunr: ctrl+o expansion — when true, collapsed thinking runs render in full\n\t// (wired into the global app.tools.expand toggle via the Expandable interface).\n\tprivate expanded = false;\n\t// lunr: live per-run timings from interactive-mode; undefined = history message\n\t// (always treated as final, rendered without durations).\n\tprivate thinkingTimings?: ThinkingRunTiming[];\n\tprivate markdownTheme: MarkdownTheme;\n\t// lunr: kept for the deferred alternative hidden-thinking indicator (Phase 8 removed the label render).\n\t// biome-ignore lint/correctness/noUnusedPrivateClassMembers: retained for future use\n\tprivate hiddenThinkingLabel: string;\n\tprivate outputPad: number;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate thinkingSource?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\t// lunr: gutter rail — prefix rendered lines with a dim │; the last line uses ╰\n\t// when this component closes the turn (no tool calls follow).\n\tprivate gutterRail: boolean;\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t\toutputPad = 1,\n\t\tgutterRail = false,\n\t\tthinkingCollapse = false,\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.thinkingCollapse = thinkingCollapse;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\t\tthis.outputPad = outputPad;\n\t\tthis.gutterRail = gutterRail;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage, this.thinkingSourceOptions());\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage, this.thinkingSourceOptions());\n\t\t}\n\t}\n\n\t// lunr: collapsible reasoning toggles (live; re-render like setHideThinkingBlock).\n\tsetThinkingCollapse(collapse: boolean): void {\n\t\tthis.thinkingCollapse = collapse;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage, this.thinkingSourceOptions());\n\t\t}\n\t}\n\n\t// lunr: ctrl+o expansion — Expandable interface hook; expanded collapsed thinking\n\t// runs render as full Markdown blocks again.\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage, this.thinkingSourceOptions());\n\t\t}\n\t}\n\n\tsetThinkingTimings(timings: ThinkingRunTiming[] | undefined): void {\n\t\tthis.thinkingTimings = timings;\n\t}\n\n\t// lunr: gutter rail toggle (live; applied on next render).\n\tsetGutterRail(enabled: boolean): void {\n\t\tthis.gutterRail = enabled;\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage, this.thinkingSourceOptions());\n\t\t}\n\t}\n\n\tsetOutputPad(padding: number): void {\n\t\tthis.outputPad = padding;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage, this.thinkingSourceOptions());\n\t\t}\n\t}\n\n\toverride render(width: number): string[] {\n\t\t// lunr: gutter rail — render content at width-2 and prefix each line with a\n\t\t// dim │; the last line uses ╰ when this assistant message closes the turn\n\t\t// (no tool calls follow). When tool calls are present the rail stays open (│)\n\t\t// because tool-execution components render their own borders below.\n\t\tconst railEnabled = this.gutterRail;\n\t\tconst contentWidth = railEnabled ? Math.max(1, width - 2) : width;\n\t\tconst lines = super.render(contentWidth);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\tif (railEnabled && lines.length > 0) {\n\t\t\t\tconst rail = theme.fg(\"dim\", \"│ \");\n\t\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\t\tlines[i] = rail + lines[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tlines[0] = OSC133_ZONE_START + lines[0];\n\t\tlines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];\n\n\t\tif (railEnabled) {\n\t\t\tconst rail = theme.fg(\"dim\", \"│ \");\n\t\t\tconst close = theme.fg(\"dim\", \"╰ \");\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tlines[i] = (i === lines.length - 1 ? close : rail) + lines[i];\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate thinkingSourceOptions(): { thinkingSource?: AssistantMessage } | undefined {\n\t\treturn this.thinkingSource ? { thinkingSource: this.thinkingSource } : undefined;\n\t}\n\n\tupdateContent(message: AssistantMessage, options?: { thinkingSource?: AssistantMessage }): void {\n\t\tthis.lastMessage = message;\n\t\tthis.thinkingSource = options?.thinkingSource;\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst sourceMessage = this.thinkingSource ?? message;\n\t\tconst sourceThinkingRuns = collectThinkingRuns(sourceMessage.content);\n\t\tconst hasVisibleContent =\n\t\t\tmessage.content.some(\n\t\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t\t) ||\n\t\t\t(!this.hideThinkingBlock && sourceThinkingRuns.some((run) => run.length > 0));\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tlet isFirstTextBlock = true;\n\t\tlet thinkingRunIndex = -1;\n\t\tlet renderedThinking = false;\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\t// lunr: leading ● marks the first text block (never thinking blocks). It rides\n\t\t\t\t// the defaultTextStyle below, so it renders bright white like the body.\n\t\t\t\tconst trimmed = content.text.trim();\n\t\t\t\tconst text = isFirstTextBlock ? `● ${trimmed}` : trimmed;\n\t\t\t\tisFirstTextBlock = false;\n\t\t\t\t// lunr: theme-polish — assistant message body renders bright white via the\n\t\t\t\t// shared userMessageText token (same knob as user messages).\n\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\tnew Markdown(text, this.outputPad, 0, this.markdownTheme, {\n\t\t\t\t\t\tcolor: (body: string) => theme.fg(\"userMessageText\", body),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} else if (content.type === \"thinking\") {\n\t\t\t\tthinkingRunIndex++;\n\t\t\t\tconst displayBlocks: string[] = [];\n\t\t\t\tfor (; i < message.content.length; i++) {\n\t\t\t\t\tconst thinkingContent = message.content[i];\n\t\t\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\t\t\tif (thinking) {\n\t\t\t\t\t\tdisplayBlocks.push(thinking);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti--;\n\n\t\t\t\tconst thinkingBlocks = sourceThinkingRuns[thinkingRunIndex] ?? displayBlocks;\n\t\t\t\tif (thinkingBlocks.length === 0 || this.hideThinkingBlock) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\trenderedThinking = true;\n\t\t\t\tthis.renderThinkingRun(thinkingRunIndex, thinkingBlocks, message, i);\n\t\t\t}\n\t\t}\n\n\t\tif (!this.hideThinkingBlock && !renderedThinking && sourceThinkingRuns.length > 0) {\n\t\t\tthinkingRunIndex = 0;\n\t\t\tconst thinkingBlocks = sourceThinkingRuns[0] ?? [];\n\t\t\tif (thinkingBlocks.length > 0) {\n\t\t\t\tthis.renderThinkingRun(thinkingRunIndex, thinkingBlocks, message, -1);\n\t\t\t}\n\t\t}\n\n\t\t// Check if incomplete/failed - show after partial content.\n\t\t// For aborted/error tool calls, tool execution components show the error.\n\t\t// Length stops can happen before a tool call is complete, so surface them here too.\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t\tif (message.stopReason === \"length\") {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.\",\n\t\t\t\t\t),\n\t\t\t\t\tthis.outputPad,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t} else if (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), this.outputPad, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), this.outputPad, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate renderThinkingRun(\n\t\tthinkingRunIndex: number,\n\t\tthinkingBlocks: string[],\n\t\tdisplayMessage: AssistantMessage,\n\t\tdisplayIndex: number,\n\t): void {\n\t\tconst hasVisibleContentAfter =\n\t\t\tdisplayIndex >= 0\n\t\t\t\t? displayMessage.content\n\t\t\t\t\t\t.slice(displayIndex + 1)\n\t\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()))\n\t\t\t\t: displayMessage.content.some((c) => c.type === \"text\" && c.text.trim());\n\n\t\tconst runComplete = isThinkingRunComplete(\n\t\t\tfalse,\n\t\t\tthis.thinkingTimings?.[thinkingRunIndex],\n\t\t\tthis.thinkingTimings !== undefined,\n\t\t);\n\t\tif (this.thinkingCollapse && !this.expanded && runComplete) {\n\t\t\tconst timing = this.thinkingTimings?.[thinkingRunIndex];\n\t\t\tconst label =\n\t\t\t\ttiming?.end !== undefined\n\t\t\t\t\t? `✻ Thought for ${formatThoughtDuration(timing.end - timing.start)}`\n\t\t\t\t\t: \"✻ Thought\";\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"thinkingText\", theme.italic(label)), this.outputPad, 0));\n\t\t\tconst snippet = thinkingSnippet(thinkingBlocks.join(\"\\n\\n\"));\n\t\t\tif (snippet) {\n\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\tnew Text(theme.fg(\"thinkingText\", theme.italic(snippet)), this.outputPad + 2, 0),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!runComplete && !this.expanded) {\n\t\t\tthis.contentContainer.addChild(\n\t\t\t\tnew ThinkingTailComponent(thinkingBlocks.join(\"\\n\\n\"), this.outputPad, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\titalic: true,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Markdown(thinkingBlocks.join(\"\\n\\n\"), this.outputPad, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\titalic: true,\n\t\t\t}),\n\t\t);\n\t\tif (hasVisibleContentAfter) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\t}\n}\n\nfunction collectThinkingRuns(content: AssistantMessage[\"content\"]): string[][] {\n\tconst runs: string[][] = [];\n\tfor (let i = 0; i < content.length; i++) {\n\t\tconst block = content[i];\n\t\tif (block.type !== \"thinking\") continue;\n\t\tconst blocks: string[] = [];\n\t\tfor (; i < content.length; i++) {\n\t\t\tconst thinkingContent = content[i];\n\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\ti--;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\tif (thinking) blocks.push(thinking);\n\t\t}\n\t\tif (blocks.length > 0) runs.push(blocks);\n\t}\n\treturn runs;\n}\n"]}
|