@agimon-ai/doompi-file-edit 0.0.1-alpha.43 → 0.0.1-alpha.45

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.
Files changed (36) hide show
  1. package/dist/adapters/EditTracker/EditTracker.cjs +1 -1
  2. package/dist/adapters/EditTracker/EditTracker.cjs.map +1 -1
  3. package/dist/adapters/EditTracker/EditTracker.mjs +1 -1
  4. package/dist/adapters/EditTracker/EditTracker.mjs.map +1 -1
  5. package/dist/adapters/TimelineStore/TimelineStore.cjs +1 -1
  6. package/dist/adapters/TimelineStore/TimelineStore.cjs.map +1 -1
  7. package/dist/adapters/TimelineStore/TimelineStore.mjs +1 -1
  8. package/dist/adapters/TimelineStore/TimelineStore.mjs.map +1 -1
  9. package/dist/adapters/node/gitStatus.cjs +2 -0
  10. package/dist/adapters/node/gitStatus.cjs.map +1 -0
  11. package/dist/adapters/node/gitStatus.mjs +2 -0
  12. package/dist/adapters/node/gitStatus.mjs.map +1 -0
  13. package/dist/adapters/webFilesChannel.cjs +1 -1
  14. package/dist/adapters/webFilesChannel.cjs.map +1 -1
  15. package/dist/adapters/webFilesChannel.d.cts.map +1 -1
  16. package/dist/adapters/webFilesChannel.d.mts.map +1 -1
  17. package/dist/adapters/webFilesChannel.mjs +1 -1
  18. package/dist/adapters/webFilesChannel.mjs.map +1 -1
  19. package/dist/container/index.cjs +1 -1
  20. package/dist/container/index.cjs.map +1 -1
  21. package/dist/container/index.d.cts.map +1 -1
  22. package/dist/container/index.d.mts.map +1 -1
  23. package/dist/container/index.mjs +1 -1
  24. package/dist/container/index.mjs.map +1 -1
  25. package/dist/services/fileChanges.cjs +2 -2
  26. package/dist/services/fileChanges.cjs.map +1 -1
  27. package/dist/services/fileChanges.mjs +2 -2
  28. package/dist/services/fileChanges.mjs.map +1 -1
  29. package/dist/types/domain.d.cts +7 -0
  30. package/dist/types/domain.d.cts.map +1 -1
  31. package/dist/types/domain.d.mts +7 -0
  32. package/dist/types/domain.d.mts.map +1 -1
  33. package/package.json +12 -12
  34. package/src/types/domain.ts +7 -0
  35. package/web/fileLinks.ts +45 -0
  36. package/web/index.ts +4 -0
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/lineDiff.cjs");let n=require("node:path");n=e.__toESM(n,1);const r=`bash`;function i(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var a=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Set;manifest;excluded=[];now;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.manifest=void 0,this.excluded=e.exclude??[]}async start(e,t,a,o){if(t===`edit`||t===`write`){let r=i(a,`path`);if(!r)return;let s=n.default.resolve(o,r);this.pending.set(e,{tool:t,filePath:s,before:await this.snapshots.capture(s)});return}t===r&&(this.bracketed.add(e),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.delete(e);if(!t){if(r){await this.recordTool(r);return}i&&await this.recordScan(n)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,t);await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n}),await this.remember(e.filePath)}async recordScan(e){let t=this.manifest;if(t===void 0)return;let n=await this.manifests.take(e,this.excluded);this.manifest=n;for(let e of this.manifests.changed(t,n))await this.append({version:2,path:e,tool:r,at:this.now(),origin:`scan`,...await this.captured(e)})}async captured(e){let t=await this.snapshots.capture(e);return t===void 0?{}:{after:t}}async countChanges(e,n){if(e===void 0||n===void 0)return{};if(e===n)return{additions:0,removals:0};let[r,i]=await Promise.all([this.snapshots.read(e),this.snapshots.read(n)]);if(r===void 0||i===void 0)return{};let a=t.lineDiff(r,i);return{additions:a.additions,removals:a.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};exports.EditTracker=a;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/lineDiff.cjs");let n=require("node:path");n=e.__toESM(n,1);const r=`bash`;function i(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var a=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Set;manifest;excluded=[];contents=new Map;now;git;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now,this.git=r.git}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.contents.clear(),this.manifest=void 0,this.excluded=e.exclude??[]}async start(e,t,a,o){if(t===`edit`||t===`write`){let r=i(a,`path`);if(!r)return;let s=n.default.resolve(o,r);this.pending.set(e,{tool:t,filePath:s,before:await this.snapshots.capture(s)});return}t===r&&(this.bracketed.add(e),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.delete(e);if(!t){if(r){await this.recordTool(r);return}i&&await this.recordScan(n)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,t);t!==void 0&&this.contents.set(e.filePath,t),await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n}),await this.remember(e.filePath)}async recordScan(e){let t=this.manifest;if(t===void 0)return;let n=await this.manifests.take(e,this.excluded);this.manifest=n;let i=this.manifests.changed(t,n);if(i.length===0)return;let a=await this.unchangedInGit(e,i);for(let e of i){let t=await this.snapshots.capture(e),n=this.contents.get(e);t!==void 0&&this.contents.set(e,t),(t===void 0||t!==n)&&(n===void 0&&a.has(e)||await this.append({version:2,path:e,tool:r,at:this.now(),origin:`scan`,verified:!0,...t===void 0?{}:{after:t}}))}}async unchangedInGit(e,t){let n=t.filter(e=>!this.contents.has(e));return this.git===void 0||n.length===0?new Set:this.git.unchanged(e,n)}async countChanges(e,n){if(e===void 0||n===void 0)return{};if(e===n)return{additions:0,removals:0};let[r,i]=await Promise.all([this.snapshots.read(e),this.snapshots.read(n)]);if(r===void 0||i===void 0)return{};let a=t.lineDiff(r,i);return{additions:a.additions,removals:a.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};exports.EditTracker=a;
2
2
  //# sourceMappingURL=EditTracker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditTracker.cjs","names":["path","lineDiff"],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n}\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Set while a bash call is in flight, so its end knows a manifest is owed. */\n private readonly bracketed = new Set<string>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n private readonly now: () => number;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[] } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, filePath, before: await this.snapshots.capture(filePath) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n this.bracketed.add(id);\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const bracketed = this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (bracketed) await this.recordScan(cwd);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const counts = await this.countChanges(pending.before, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever moved while a bash call ran, however the command wrote it. */\n private async recordScan(cwd: string): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n for (const filePath of this.manifests.changed(before, after)) {\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n ...(await this.captured(filePath)),\n });\n }\n }\n\n /** The after-snapshot of a scan-found path, when it still exists and fits. */\n private async captured(filePath: string): Promise<{ after?: string }> {\n const after = await this.snapshots.capture(filePath);\n return after === undefined ? {} : { after };\n }\n\n /** How many lines moved, when both sides were captured. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n ): Promise<{ additions?: number; removals?: number }> {\n if (before === undefined || after === undefined) return {};\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
1
+ {"version":3,"file":"EditTracker.cjs","names":["path","lineDiff"],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n *\n * A manifest compares size and modification time, which a checkout, an install,\n * or a formatter rewriting identical bytes moves without changing a single\n * byte of content. So a candidate is confirmed before it is recorded: its\n * content hash is compared against the last one this session saw, and a path\n * this session has never captured is put to git. Neither answer is available\n * for every file, an ignored temporary file is unknown to git and a first\n * sighting has no earlier hash, so an unconfirmable candidate is still\n * recorded; only a proven non-change is dropped.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n /**\n * Decides whether a first-seen scan candidate actually changed. Optional:\n * without it the tracker keeps recording every candidate it cannot disprove.\n */\n git?: GitStatusPort;\n}\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Set while a bash call is in flight, so its end knows a manifest is owed. */\n private readonly bracketed = new Set<string>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n /**\n * The last content hash this session saw per path, from either mechanism.\n * A candidate whose hash has not moved was touched, not edited.\n */\n private readonly contents = new Map<string, string>();\n private readonly now: () => number;\n private readonly git: GitStatusPort | undefined;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n this.git = options.git;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[] } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.contents.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, filePath, before: await this.snapshots.capture(filePath) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n this.bracketed.add(id);\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const bracketed = this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (bracketed) await this.recordScan(cwd);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const counts = await this.countChanges(pending.before, after);\n if (after !== undefined) this.contents.set(pending.filePath, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever a bash call actually edited, however the command wrote it. */\n private async recordScan(cwd: string): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n const candidates = this.manifests.changed(before, after);\n if (candidates.length === 0) return;\n const unchanged = await this.unchangedInGit(cwd, candidates);\n for (const filePath of candidates) {\n const captured = await this.snapshots.capture(filePath);\n const known = this.contents.get(filePath);\n if (captured !== undefined) this.contents.set(filePath, captured);\n // The bytes are the ones this session already recorded: the call moved the\n // modification time and nothing else.\n if (captured !== undefined && captured === known) continue;\n // Never captured here, but git still holds the file and says it matches.\n if (known === undefined && unchanged.has(filePath)) continue;\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n verified: true,\n ...(captured === undefined ? {} : { after: captured }),\n });\n }\n }\n\n /** Which candidates git tracks and reports as untouched; empty when it cannot say. */\n private async unchangedInGit(cwd: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {\n const unseen = candidates.filter((filePath) => !this.contents.has(filePath));\n if (this.git === undefined || unseen.length === 0) return new Set();\n return this.git.unchanged(cwd, unseen);\n }\n\n /** How many lines moved, when both sides were captured. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n ): Promise<{ additions?: number; removals?: number }> {\n if (before === undefined || after === undefined) return {};\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{lineDiff as e}from"../../services/lineDiff.mjs";import t from"node:path";const n=`bash`;function r(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var i=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Set;manifest;excluded=[];now;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.manifest=void 0,this.excluded=e.exclude??[]}async start(e,i,a,o){if(i===`edit`||i===`write`){let n=r(a,`path`);if(!n)return;let s=t.resolve(o,n);this.pending.set(e,{tool:i,filePath:s,before:await this.snapshots.capture(s)});return}i===n&&(this.bracketed.add(e),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.delete(e);if(!t){if(r){await this.recordTool(r);return}i&&await this.recordScan(n)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,t);await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n}),await this.remember(e.filePath)}async recordScan(e){let t=this.manifest;if(t===void 0)return;let r=await this.manifests.take(e,this.excluded);this.manifest=r;for(let e of this.manifests.changed(t,r))await this.append({version:2,path:e,tool:n,at:this.now(),origin:`scan`,...await this.captured(e)})}async captured(e){let t=await this.snapshots.capture(e);return t===void 0?{}:{after:t}}async countChanges(t,n){if(t===void 0||n===void 0)return{};if(t===n)return{additions:0,removals:0};let[r,i]=await Promise.all([this.snapshots.read(t),this.snapshots.read(n)]);if(r===void 0||i===void 0)return{};let a=e(r,i);return{additions:a.additions,removals:a.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};export{i as EditTracker};
1
+ import{lineDiff as e}from"../../services/lineDiff.mjs";import t from"node:path";const n=`bash`;function r(e,t){if(!e||typeof e!=`object`)return;let n=e[t];return typeof n==`string`?n:void 0}var i=class{timeline;snapshots;manifests;pending=new Map;bracketed=new Set;manifest;excluded=[];contents=new Map;now;git;constructor(e,t,n,r={}){this.timeline=e,this.snapshots=t,this.manifests=n,this.now=r.now??Date.now,this.git=r.git}reset(e={}){this.pending.clear(),this.bracketed.clear(),this.contents.clear(),this.manifest=void 0,this.excluded=e.exclude??[]}async start(e,i,a,o){if(i===`edit`||i===`write`){let n=r(a,`path`);if(!n)return;let s=t.resolve(o,n);this.pending.set(e,{tool:i,filePath:s,before:await this.snapshots.capture(s)});return}i===n&&(this.bracketed.add(e),this.manifest??=await this.manifests.take(o,this.excluded))}async end(e,t,n){let r=this.pending.get(e);this.pending.delete(e);let i=this.bracketed.delete(e);if(!t){if(r){await this.recordTool(r);return}i&&await this.recordScan(n)}}async recordTool(e){let t=await this.snapshots.capture(e.filePath),n=await this.countChanges(e.before,t);t!==void 0&&this.contents.set(e.filePath,t),await this.append({version:2,path:e.filePath,tool:e.tool,at:this.now(),origin:`tool`,...e.before===void 0?{}:{before:e.before},...t===void 0?{}:{after:t},...n}),await this.remember(e.filePath)}async recordScan(e){let t=this.manifest;if(t===void 0)return;let r=await this.manifests.take(e,this.excluded);this.manifest=r;let i=this.manifests.changed(t,r);if(i.length===0)return;let a=await this.unchangedInGit(e,i);for(let e of i){let t=await this.snapshots.capture(e),r=this.contents.get(e);t!==void 0&&this.contents.set(e,t),(t===void 0||t!==r)&&(r===void 0&&a.has(e)||await this.append({version:2,path:e,tool:n,at:this.now(),origin:`scan`,verified:!0,...t===void 0?{}:{after:t}}))}}async unchangedInGit(e,t){let n=t.filter(e=>!this.contents.has(e));return this.git===void 0||n.length===0?new Set:this.git.unchanged(e,n)}async countChanges(t,n){if(t===void 0||n===void 0)return{};if(t===n)return{additions:0,removals:0};let[r,i]=await Promise.all([this.snapshots.read(t),this.snapshots.read(n)]);if(r===void 0||i===void 0)return{};let a=e(r,i);return{additions:a.additions,removals:a.removals}}async append(e){(e.before===void 0||e.before!==e.after)&&await this.timeline.append(e)}async remember(e){if(this.manifest===void 0)return;let t=new Map(this.manifest.entries),n=await this.manifests.fingerprint(e);n===void 0?t.delete(e):t.set(e,n),this.manifest={entries:t,truncated:this.manifest.truncated}}};export{i as EditTracker};
2
2
  //# sourceMappingURL=EditTracker.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditTracker.mjs","names":[],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n}\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Set while a bash call is in flight, so its end knows a manifest is owed. */\n private readonly bracketed = new Set<string>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n private readonly now: () => number;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[] } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, filePath, before: await this.snapshots.capture(filePath) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n this.bracketed.add(id);\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const bracketed = this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (bracketed) await this.recordScan(cwd);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const counts = await this.countChanges(pending.before, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever moved while a bash call ran, however the command wrote it. */\n private async recordScan(cwd: string): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n for (const filePath of this.manifests.changed(before, after)) {\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n ...(await this.captured(filePath)),\n });\n }\n }\n\n /** The after-snapshot of a scan-found path, when it still exists and fits. */\n private async captured(filePath: string): Promise<{ after?: string }> {\n const after = await this.snapshots.capture(filePath);\n return after === undefined ? {} : { after };\n }\n\n /** How many lines moved, when both sides were captured. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n ): Promise<{ additions?: number; removals?: number }> {\n if (before === undefined || after === undefined) return {};\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
1
+ {"version":3,"file":"EditTracker.mjs","names":[],"sources":["../../../src/adapters/EditTracker/EditTracker.ts"],"sourcesContent":["import path from 'node:path';\nimport { lineDiff } from '../../services/lineDiff.ts';\nimport type { FileEditTool, TimelineEvent } from '../../types/domain';\nimport type { IEditTracker } from '../../types/editTracker';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\nimport type { SnapshotStorePort } from '../../types/snapshotStore.ts';\nimport type { ITimelineStore } from '../../types/timelineStore';\nimport type { TreeManifest, TreeManifestPort } from '../../types/treeManifest.ts';\n\nconst EDIT_TOOL = 'edit';\nconst WRITE_TOOL = 'write';\nconst BASH_TOOL = 'bash';\n\n/**\n * Records what the session changed, and captures enough of it to be diffed.\n *\n * Two mechanisms, because one is not enough. `edit` and `write` name their file\n * in the call arguments, so the content is read before the tool runs and both\n * sides of the change are known exactly. `bash` names nothing reliable: the\n * agent can write a script and run it, and reading path-shaped tokens out of\n * the command would miss every file that script touches. So a bash call is\n * bracketed by tree manifests instead, and whatever moved between them is\n * recorded, however it was written.\n *\n * A manifest-found path is recorded without a baseline, because it was only\n * identified after it had already changed. That is a real limit and the wire\n * carries it as `origin: 'scan'` rather than pretending a diff exists.\n *\n * A manifest compares size and modification time, which a checkout, an install,\n * or a formatter rewriting identical bytes moves without changing a single\n * byte of content. So a candidate is confirmed before it is recorded: its\n * content hash is compared against the last one this session saw, and a path\n * this session has never captured is put to git. Neither answer is available\n * for every file, an ignored temporary file is unknown to git and a first\n * sighting has no earlier hash, so an unconfirmable candidate is still\n * recorded; only a proven non-change is dropped.\n */\ninterface PendingEdit {\n tool: 'edit' | 'write';\n filePath: string;\n before: string | undefined;\n}\n\nfunction objectValue(value: unknown, key: string): string | undefined {\n if (!value || typeof value !== 'object') return undefined;\n const found = (value as Record<string, unknown>)[key];\n return typeof found === 'string' ? found : undefined;\n}\n\nexport interface EditTrackerOptions {\n /** Injectable so a test can pin the recorded timestamps. */\n now?: () => number;\n /**\n * Decides whether a first-seen scan candidate actually changed. Optional:\n * without it the tracker keeps recording every candidate it cannot disprove.\n */\n git?: GitStatusPort;\n}\n\nexport class EditTracker implements IEditTracker {\n private readonly pending = new Map<string, PendingEdit>();\n /** Set while a bash call is in flight, so its end knows a manifest is owed. */\n private readonly bracketed = new Set<string>();\n /**\n * The tree as this tracker last saw it. A bash call compares against it and\n * then replaces it, so each call costs one walk rather than two, and a change\n * made between calls is still noticed on the next one.\n */\n private manifest: TreeManifest | undefined;\n /**\n * This package's own storage, which the walk must never report. The timeline\n * and its snapshots can land inside the tree being watched, and recording\n * them would make every change cause another one.\n */\n private excluded: readonly string[] = [];\n /**\n * The last content hash this session saw per path, from either mechanism.\n * A candidate whose hash has not moved was touched, not edited.\n */\n private readonly contents = new Map<string, string>();\n private readonly now: () => number;\n private readonly git: GitStatusPort | undefined;\n\n constructor(\n private readonly timeline: ITimelineStore,\n private readonly snapshots: SnapshotStorePort,\n private readonly manifests: TreeManifestPort,\n options: EditTrackerOptions = {},\n ) {\n this.now = options.now ?? Date.now;\n this.git = options.git;\n }\n\n /**\n * Forgets the previous session's tree so a new one does not inherit its\n * baseline, and takes the paths this session's own bookkeeping occupies.\n */\n reset(options: { exclude?: readonly string[] } = {}): void {\n this.pending.clear();\n this.bracketed.clear();\n this.contents.clear();\n this.manifest = undefined;\n this.excluded = options.exclude ?? [];\n }\n\n async start(id: string, tool: string, args: unknown, cwd: string): Promise<void> {\n if (tool === EDIT_TOOL || tool === WRITE_TOOL) {\n const supplied = objectValue(args, 'path');\n if (!supplied) return;\n const filePath = path.resolve(cwd, supplied);\n this.pending.set(id, { tool, filePath, before: await this.snapshots.capture(filePath) });\n return;\n }\n if (tool !== BASH_TOOL) return;\n this.bracketed.add(id);\n // The first bash call of a session has nothing to compare against, so it\n // pays for the baseline walk; every later call reuses the previous end.\n this.manifest ??= await this.manifests.take(cwd, this.excluded);\n }\n\n async end(id: string, isError: boolean, cwd: string): Promise<void> {\n const pending = this.pending.get(id);\n this.pending.delete(id);\n const bracketed = this.bracketed.delete(id);\n if (isError) return;\n if (pending) {\n await this.recordTool(pending);\n return;\n }\n if (bracketed) await this.recordScan(cwd);\n }\n\n /** An `edit` or `write` whose file was read on both sides of the call. */\n private async recordTool(pending: PendingEdit): Promise<void> {\n const after = await this.snapshots.capture(pending.filePath);\n const counts = await this.countChanges(pending.before, after);\n if (after !== undefined) this.contents.set(pending.filePath, after);\n await this.append({\n version: 2,\n path: pending.filePath,\n tool: pending.tool,\n at: this.now(),\n origin: 'tool',\n ...(pending.before === undefined ? {} : { before: pending.before }),\n ...(after === undefined ? {} : { after }),\n ...counts,\n });\n // The tool already accounted for this path, so the next bash comparison\n // must not report it a second time under its own name.\n await this.remember(pending.filePath);\n }\n\n /** Whatever a bash call actually edited, however the command wrote it. */\n private async recordScan(cwd: string): Promise<void> {\n const before = this.manifest;\n if (before === undefined) return;\n const after = await this.manifests.take(cwd, this.excluded);\n this.manifest = after;\n const candidates = this.manifests.changed(before, after);\n if (candidates.length === 0) return;\n const unchanged = await this.unchangedInGit(cwd, candidates);\n for (const filePath of candidates) {\n const captured = await this.snapshots.capture(filePath);\n const known = this.contents.get(filePath);\n if (captured !== undefined) this.contents.set(filePath, captured);\n // The bytes are the ones this session already recorded: the call moved the\n // modification time and nothing else.\n if (captured !== undefined && captured === known) continue;\n // Never captured here, but git still holds the file and says it matches.\n if (known === undefined && unchanged.has(filePath)) continue;\n await this.append({\n version: 2,\n path: filePath,\n tool: BASH_TOOL,\n at: this.now(),\n origin: 'scan',\n verified: true,\n ...(captured === undefined ? {} : { after: captured }),\n });\n }\n }\n\n /** Which candidates git tracks and reports as untouched; empty when it cannot say. */\n private async unchangedInGit(cwd: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {\n const unseen = candidates.filter((filePath) => !this.contents.has(filePath));\n if (this.git === undefined || unseen.length === 0) return new Set();\n return this.git.unchanged(cwd, unseen);\n }\n\n /** How many lines moved, when both sides were captured. */\n private async countChanges(\n before: string | undefined,\n after: string | undefined,\n ): Promise<{ additions?: number; removals?: number }> {\n if (before === undefined || after === undefined) return {};\n if (before === after) return { additions: 0, removals: 0 };\n const [beforeText, afterText] = await Promise.all([this.snapshots.read(before), this.snapshots.read(after)]);\n if (beforeText === undefined || afterText === undefined) return {};\n const diff = lineDiff(beforeText, afterText);\n return { additions: diff.additions, removals: diff.removals };\n }\n\n /** Records the change unless it turned out to be no change at all. */\n private async append(event: TimelineEvent): Promise<void> {\n if (event.before !== undefined && event.before === event.after) return;\n await this.timeline.append(event);\n }\n\n /**\n * Folds a path the tool layer just handled into the tracked manifest, so the\n * next bash comparison sees it as already accounted for.\n *\n * The fingerprint has to be the one a walk would read, not a stand-in, or the\n * next comparison reports the file again under bash and every tool edit is\n * recorded twice.\n */\n private async remember(filePath: string): Promise<void> {\n if (this.manifest === undefined) return;\n const entries = new Map(this.manifest.entries);\n const fingerprint = await this.manifests.fingerprint(filePath);\n if (fingerprint === undefined) entries.delete(filePath);\n else entries.set(filePath, fingerprint);\n this.manifest = { entries, truncated: this.manifest.truncated };\n }\n}\n\n/** The tools this tracker knows how to attribute a change to. */\nexport const TRACKED_TOOLS: readonly FileEditTool[] = [EDIT_TOOL, WRITE_TOOL, BASH_TOOL];\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/fileChanges.cjs");let n=require("node:fs/promises");n=e.__toESM(n,1);function r(e,t){return e instanceof Error&&`code`in e&&e.code===t}var i=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>n.default.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t.foldEntries(await this.events())}async versions(e){return t.foldVersions(await this.events(),e)}async clear(){try{await n.default.unlink(this.requirePath())}catch(e){if(!r(e,`ENOENT`))throw e}}async events(){let e;try{e=await n.default.readFile(this.requirePath(),`utf8`)}catch(e){if(r(e,`ENOENT`))return[];throw e}return t.parseTimeline(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`;for(let i=0;i<100;i+=1)try{let i=await n.default.open(t,`wx`);try{return await e()}finally{await i.close();try{await n.default.unlink(t)}catch(e){r(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!r(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${t}`)}};exports.TimelineStore=i;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/fileChanges.cjs");let n=require("node:fs/promises");n=e.__toESM(n,1);function r(e,t){return e instanceof Error&&`code`in e&&e.code===t}var i=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>n.default.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t.foldEntries(t.confirmedChanges(await this.events()))}async versions(e){return t.foldVersions(await this.events(),e)}async clear(){try{await n.default.unlink(this.requirePath())}catch(e){if(!r(e,`ENOENT`))throw e}}async events(){let e;try{e=await n.default.readFile(this.requirePath(),`utf8`)}catch(e){if(r(e,`ENOENT`))return[];throw e}return t.parseTimeline(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`;for(let i=0;i<100;i+=1)try{let i=await n.default.open(t,`wx`);try{return await e()}finally{await i.close();try{await n.default.unlink(t)}catch(e){r(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!r(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${t}`)}};exports.TimelineStore=i;
2
2
  //# sourceMappingURL=TimelineStore.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineStore.cjs","names":["fs","foldEntries","foldVersions","parseTimeline"],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport { type AnyTimelineEvent, foldEntries, foldVersions, parseTimeline } from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n return foldEntries(await this.events());\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n return foldVersions(await this.events(), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n}\n"],"mappings":"uJAQA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAYA,EAAAA,QAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CACrC,OAAOC,EAAAA,YAAY,MAAM,KAAK,OAAO,CAAC,CACxC,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAOC,EAAAA,aAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAMF,EAAAA,QAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAMA,EAAAA,QAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAOG,EAAAA,cAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAMH,EAAAA,QAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
1
+ {"version":3,"file":"TimelineStore.cjs","names":["fs","foldEntries","confirmedChanges","foldVersions","parseTimeline"],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport {\n type AnyTimelineEvent,\n confirmedChanges,\n foldEntries,\n foldVersions,\n parseTimeline,\n} from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n // Listing shows what was edited, so a path a command only touched is left out.\n return foldEntries(confirmedChanges(await this.events()));\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n return foldVersions(await this.events(), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n}\n"],"mappings":"uJAcA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAYA,EAAAA,QAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CAErC,OAAOC,EAAAA,YAAYC,EAAAA,iBAAiB,MAAM,KAAK,OAAO,CAAC,CAAC,CAC1D,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAOC,EAAAA,aAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAMH,EAAAA,QAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAMA,EAAAA,QAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAOI,EAAAA,cAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAMJ,EAAAA,QAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAMA,EAAAA,QAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
@@ -1,2 +1,2 @@
1
- import{foldEntries as e,foldVersions as t,parseTimeline as n}from"../../services/fileChanges.mjs";import r from"node:fs/promises";function i(e,t){return e instanceof Error&&`code`in e&&e.code===t}var a=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>r.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return e(await this.events())}async versions(e){return t(await this.events(),e)}async clear(){try{await r.unlink(this.requirePath())}catch(e){if(!i(e,`ENOENT`))throw e}}async events(){let e;try{e=await r.readFile(this.requirePath(),`utf8`)}catch(e){if(i(e,`ENOENT`))return[];throw e}return n(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`;for(let n=0;n<100;n+=1)try{let n=await r.open(t,`wx`);try{return await e()}finally{await n.close();try{await r.unlink(t)}catch(e){i(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!i(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${t}`)}};export{a as TimelineStore};
1
+ import{confirmedChanges as e,foldEntries as t,foldVersions as n,parseTimeline as r}from"../../services/fileChanges.mjs";import i from"node:fs/promises";function a(e,t){return e instanceof Error&&`code`in e&&e.code===t}var o=class{filePath;initialize(e){this.filePath=e}async append(e){await this.withLock(async()=>i.appendFile(this.requirePath(),`${JSON.stringify(e)}\n`,`utf8`))}async list(){return t(e(await this.events()))}async versions(e){return n(await this.events(),e)}async clear(){try{await i.unlink(this.requirePath())}catch(e){if(!a(e,`ENOENT`))throw e}}async events(){let e;try{e=await i.readFile(this.requirePath(),`utf8`)}catch(e){if(a(e,`ENOENT`))return[];throw e}return r(e,e=>console.warn(`Ignoring malformed file edit timeline line: ${e.slice(0,200)}`))}requirePath(){if(!this.filePath)throw Error(`Timeline store is not initialized`);return this.filePath}async withLock(e){let t=`${this.requirePath()}.lock`;for(let n=0;n<100;n+=1)try{let n=await i.open(t,`wx`);try{return await e()}finally{await n.close();try{await i.unlink(t)}catch(e){a(e,`ENOENT`)||console.warn(`Could not remove timeline lock: ${String(e)}`)}}}catch(e){if(!a(e,`EEXIST`))throw e;await new Promise(e=>setTimeout(e,10))}throw Error(`Timed out acquiring timeline lock ${t}`)}};export{o as TimelineStore};
2
2
  //# sourceMappingURL=TimelineStore.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineStore.mjs","names":[],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport { type AnyTimelineEvent, foldEntries, foldVersions, parseTimeline } from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n return foldEntries(await this.events());\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n return foldVersions(await this.events(), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n}\n"],"mappings":"kIAQA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAY,EAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CACrC,OAAO,EAAY,MAAM,KAAK,OAAO,CAAC,CACxC,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAO,EAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAM,EAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAM,EAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAO,EAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAM,EAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAM,EAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
1
+ {"version":3,"file":"TimelineStore.mjs","names":[],"sources":["../../../src/adapters/TimelineStore/TimelineStore.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport {\n type AnyTimelineEvent,\n confirmedChanges,\n foldEntries,\n foldVersions,\n parseTimeline,\n} from '../../services/fileChanges.ts';\nimport type { FileEditEntry, FileEditVersion, TimelineEvent } from '../../types/domain';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\nconst LOCK_RETRY_MS = 10;\nconst LOCK_RETRIES = 100;\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nexport class TimelineStore implements ITimelineStore {\n private filePath: string | undefined;\n\n initialize(filePath: string): void {\n this.filePath = filePath;\n }\n\n async append(event: TimelineEvent): Promise<void> {\n await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\\n`, 'utf8'));\n }\n\n async list(): Promise<FileEditEntry[]> {\n // Listing shows what was edited, so a path a command only touched is left out.\n return foldEntries(confirmedChanges(await this.events()));\n }\n\n async versions(filePath: string): Promise<FileEditVersion[]> {\n return foldVersions(await this.events(), filePath);\n }\n\n async clear(): Promise<void> {\n try {\n await fs.unlink(this.requirePath());\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) throw error;\n }\n }\n\n /** Every recorded change, or none at all when the session has not written yet. */\n private async events(): Promise<AnyTimelineEvent[]> {\n let content: string;\n try {\n content = await fs.readFile(this.requirePath(), 'utf8');\n } catch (error) {\n if (hasCode(error, 'ENOENT')) return [];\n throw error;\n }\n return parseTimeline(content, (line) =>\n console.warn(`Ignoring malformed file edit timeline line: ${line.slice(0, 200)}`),\n );\n }\n\n private requirePath(): string {\n if (!this.filePath) throw new Error('Timeline store is not initialized');\n return this.filePath;\n }\n\n private async withLock<T>(operation: () => Promise<T>): Promise<T> {\n const lockPath = `${this.requirePath()}.lock`;\n for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {\n try {\n const lock = await fs.open(lockPath, 'wx');\n try {\n return await operation();\n } finally {\n await lock.close();\n try {\n await fs.unlink(lockPath);\n } catch (error) {\n if (!hasCode(error, 'ENOENT')) console.warn(`Could not remove timeline lock: ${String(error)}`);\n }\n }\n } catch (error) {\n if (!hasCode(error, 'EEXIST')) throw error;\n await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));\n }\n }\n throw new Error(`Timed out acquiring timeline lock ${lockPath}`);\n }\n}\n"],"mappings":"wJAcA,SAAS,EAAQ,EAAgB,EAAuB,CACtD,OAAO,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,CACrE,CAEA,IAAa,EAAb,KAAqD,CACnD,SAEA,WAAW,EAAwB,CACjC,KAAK,SAAW,CAClB,CAEA,MAAM,OAAO,EAAqC,CAChD,MAAM,KAAK,SAAS,SAAY,EAAG,WAAW,KAAK,YAAY,EAAG,GAAG,KAAK,UAAU,CAAK,EAAE,IAAK,MAAM,CAAC,CACzG,CAEA,MAAM,MAAiC,CAErC,OAAO,EAAY,EAAiB,MAAM,KAAK,OAAO,CAAC,CAAC,CAC1D,CAEA,MAAM,SAAS,EAA8C,CAC3D,OAAO,EAAa,MAAM,KAAK,OAAO,EAAG,CAAQ,CACnD,CAEA,MAAM,OAAuB,CAC3B,GAAI,CACF,MAAM,EAAG,OAAO,KAAK,YAAY,CAAC,CACpC,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,CACvC,CACF,CAGA,MAAc,QAAsC,CAClD,IAAI,EACJ,GAAI,CACF,EAAU,MAAM,EAAG,SAAS,KAAK,YAAY,EAAG,MAAM,CACxD,OAAS,EAAO,CACd,GAAI,EAAQ,EAAO,QAAQ,EAAG,MAAO,CAAC,EACtC,MAAM,CACR,CACA,OAAO,EAAc,EAAU,GAC7B,QAAQ,KAAK,+CAA+C,EAAK,MAAM,EAAG,GAAG,GAAG,CAClF,CACF,CAEA,aAA8B,CAC5B,GAAI,CAAC,KAAK,SAAU,MAAU,MAAM,mCAAmC,EACvE,OAAO,KAAK,QACd,CAEA,MAAc,SAAY,EAAyC,CACjE,IAAM,EAAW,GAAG,KAAK,YAAY,EAAE,OACvC,IAAK,IAAI,EAAU,EAAG,EAAU,IAAc,GAAW,EACvD,GAAI,CACF,IAAM,EAAO,MAAM,EAAG,KAAK,EAAU,IAAI,EACzC,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,QAAU,CACR,MAAM,EAAK,MAAM,EACjB,GAAI,CACF,MAAM,EAAG,OAAO,CAAQ,CAC1B,OAAS,EAAO,CACT,EAAQ,EAAO,QAAQ,GAAG,QAAQ,KAAK,mCAAmC,OAAO,CAAK,GAAG,CAChG,CACF,CACF,OAAS,EAAO,CACd,GAAI,CAAC,EAAQ,EAAO,QAAQ,EAAG,MAAM,EACrC,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAa,CAAC,CACnE,CAEF,MAAU,MAAM,qCAAqC,GAAU,CACjE,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:child_process");const r=(0,require("node:util").promisify)(n.execFile);function i(e,t){let n=[];for(let r=0;r<e.length;r+=t)n.push(e.slice(r,r+t));return n}function a(e){return e.split(`\0`).filter(e=>e!==``)}function o(e){return a(e).map(e=>e.length>3&&e[2]===` `?e.slice(3):e)}function s(e,n){let r=t.default.relative(e,n);return r!==``&&!r.startsWith(`..`)&&!t.default.isAbsolute(r)}var c=class{async unchanged(e,n){let r=new Set;if(n.length===0)return r;let c=await this.topLevel(e);if(c===void 0)return r;for(let e of i(n.filter(e=>s(c,e)),200)){let n=e.map(e=>t.default.relative(c,e)),i=await this.git(c,[`ls-files`,`-z`,`--`,...n]);if(i===void 0)continue;let s=await this.git(c,[`status`,`--porcelain`,`-z`,`--untracked-files=no`,`--`,...n]);if(s===void 0)continue;let l=new Set(o(s));for(let e of a(i))l.has(e)||r.add(t.default.resolve(c,e))}return r}async topLevel(e){let n=(await this.git(e,[`rev-parse`,`--show-toplevel`]))?.trim();return n===void 0||n===``?void 0:t.default.resolve(n)}async git(e,t){try{let{stdout:n}=await r(`git`,[...t],{cwd:e,encoding:`utf8`,maxBuffer:4194304});return n}catch{return}}};exports.NodeGitStatusAdapter=c;
2
+ //# sourceMappingURL=gitStatus.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitStatus.cjs","names":["promisify","execFile","path"],"sources":["../../../src/adapters/node/gitStatus.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\n\n/**\n * Answers the unchanged question with git, in two batched calls per chunk.\n *\n * One technology per adapter. This is where git enters the scan path; the\n * tracker that depends on the capability keeps importing the port.\n *\n * `ls-files` names which of the given paths git tracks at all, and `status`\n * names which of those moved. Everything else, an ignored build artefact, a\n * temporary file, a tree with no repository, falls out of both answers and is\n * therefore never reported as unchanged: the caller must keep recording it,\n * because git has nothing to say about a file it never saw.\n */\n\n/** Paths per git invocation, so a large scan cannot overflow the argument list. */\nconst CHUNK = 200;\nconst MAX_BUFFER = 4 * 1024 * 1024;\n\nconst run = promisify(execFile);\n\nfunction chunked<T>(values: readonly T[], size: number): T[][] {\n const groups: T[][] = [];\n for (let index = 0; index < values.length; index += size) groups.push(values.slice(index, index + size));\n return groups;\n}\n\nfunction splitRecords(output: string): string[] {\n return output.split('\\0').filter((entry) => entry !== '');\n}\n\n/**\n * The paths named by `status --porcelain -z`, whose records carry a two-letter\n * code and a space. A rename adds a bare second record; keeping it is harmless,\n * because every path in this set is one the caller records anyway.\n */\nfunction statusPaths(output: string): string[] {\n return splitRecords(output).map((record) => (record.length > 3 && record[2] === ' ' ? record.slice(3) : record));\n}\n\nfunction isInside(root: string, filePath: string): boolean {\n const relative = path.relative(root, filePath);\n return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nexport class NodeGitStatusAdapter implements GitStatusPort {\n async unchanged(cwd: string, filePaths: readonly string[]): Promise<ReadonlySet<string>> {\n const clean = new Set<string>();\n if (filePaths.length === 0) return clean;\n const root = await this.topLevel(cwd);\n if (root === undefined) return clean;\n\n for (const group of chunked(\n filePaths.filter((filePath) => isInside(root, filePath)),\n CHUNK,\n )) {\n const relative = group.map((filePath) => path.relative(root, filePath));\n const tracked = await this.git(root, ['ls-files', '-z', '--', ...relative]);\n if (tracked === undefined) continue;\n const dirty = await this.git(root, ['status', '--porcelain', '-z', '--untracked-files=no', '--', ...relative]);\n if (dirty === undefined) continue;\n const moved = new Set(statusPaths(dirty));\n for (const entry of splitRecords(tracked)) {\n if (!moved.has(entry)) clean.add(path.resolve(root, entry));\n }\n }\n return clean;\n }\n\n /** The repository root, or undefined when this tree is not one. */\n private async topLevel(cwd: string): Promise<string | undefined> {\n const output = await this.git(cwd, ['rev-parse', '--show-toplevel']);\n const root = output?.trim();\n return root === undefined || root === '' ? undefined : path.resolve(root);\n }\n\n /** Git's stdout, or undefined when the call failed for any reason at all. */\n private async git(cwd: string, args: readonly string[]): Promise<string | undefined> {\n try {\n const { stdout } = await run('git', [...args], { cwd, encoding: 'utf8', maxBuffer: MAX_BUFFER });\n return stdout;\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"wIAmBA,MAGM,GAAA,qBAAMA,CAAAA,CAAAA,UAAAA,CAAUC,EAAAA,QAAQ,EAE9B,SAAS,EAAW,EAAsB,EAAqB,CAC7D,IAAM,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAAM,EAAO,KAAK,EAAO,MAAM,EAAO,EAAQ,CAAI,CAAC,EACvG,OAAO,CACT,CAEA,SAAS,EAAa,EAA0B,CAC9C,OAAO,EAAO,MAAM,IAAI,CAAC,CAAC,OAAQ,GAAU,IAAU,EAAE,CAC1D,CAOA,SAAS,EAAY,EAA0B,CAC7C,OAAO,EAAa,CAAM,CAAC,CAAC,IAAK,GAAY,EAAO,OAAS,GAAK,EAAO,KAAO,IAAM,EAAO,MAAM,CAAC,EAAI,CAAO,CACjH,CAEA,SAAS,EAAS,EAAc,EAA2B,CACzD,IAAM,EAAWC,EAAAA,QAAK,SAAS,EAAM,CAAQ,EAC7C,OAAO,IAAa,IAAM,CAAC,EAAS,WAAW,IAAI,GAAK,CAACA,EAAAA,QAAK,WAAW,CAAQ,CACnF,CAEA,IAAa,EAAb,KAA2D,CACzD,MAAM,UAAU,EAAa,EAA4D,CACvF,IAAM,EAAQ,IAAI,IAClB,GAAI,EAAU,SAAW,EAAG,OAAO,EACnC,IAAM,EAAO,MAAM,KAAK,SAAS,CAAG,EACpC,GAAI,IAAS,IAAA,GAAW,OAAO,EAE/B,IAAK,IAAM,KAAS,EAClB,EAAU,OAAQ,GAAa,EAAS,EAAM,CAAQ,CAAC,EACvD,GACF,EAAG,CACD,IAAM,EAAW,EAAM,IAAK,GAAaA,EAAAA,QAAK,SAAS,EAAM,CAAQ,CAAC,EAChE,EAAU,MAAM,KAAK,IAAI,EAAM,CAAC,WAAY,KAAM,KAAM,GAAG,CAAQ,CAAC,EAC1E,GAAI,IAAY,IAAA,GAAW,SAC3B,IAAM,EAAQ,MAAM,KAAK,IAAI,EAAM,CAAC,SAAU,cAAe,KAAM,uBAAwB,KAAM,GAAG,CAAQ,CAAC,EAC7G,GAAI,IAAU,IAAA,GAAW,SACzB,IAAM,EAAQ,IAAI,IAAI,EAAY,CAAK,CAAC,EACxC,IAAK,IAAM,KAAS,EAAa,CAAO,EACjC,EAAM,IAAI,CAAK,GAAG,EAAM,IAAIA,EAAAA,QAAK,QAAQ,EAAM,CAAK,CAAC,CAE9D,CACA,OAAO,CACT,CAGA,MAAc,SAAS,EAA0C,CAE/D,IAAM,GAAO,MADQ,KAAK,IAAI,EAAK,CAAC,YAAa,iBAAiB,CAAC,EAAA,EAC9C,KAAK,EAC1B,OAAO,IAAS,IAAA,IAAa,IAAS,GAAK,IAAA,GAAYA,EAAAA,QAAK,QAAQ,CAAI,CAC1E,CAGA,MAAc,IAAI,EAAa,EAAsD,CACnF,GAAI,CACF,GAAM,CAAE,UAAW,MAAM,EAAI,MAAO,CAAC,GAAG,CAAI,EAAG,CAAE,MAAK,SAAU,OAAQ,UAAW,OAAW,CAAC,EAC/F,OAAO,CACT,MAAQ,CACN,MACF,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ import e from"node:path";import{execFile as t}from"node:child_process";import{promisify as n}from"node:util";const r=n(t);function i(e,t){let n=[];for(let r=0;r<e.length;r+=t)n.push(e.slice(r,r+t));return n}function a(e){return e.split(`\0`).filter(e=>e!==``)}function o(e){return a(e).map(e=>e.length>3&&e[2]===` `?e.slice(3):e)}function s(t,n){let r=e.relative(t,n);return r!==``&&!r.startsWith(`..`)&&!e.isAbsolute(r)}var c=class{async unchanged(t,n){let r=new Set;if(n.length===0)return r;let c=await this.topLevel(t);if(c===void 0)return r;for(let t of i(n.filter(e=>s(c,e)),200)){let n=t.map(t=>e.relative(c,t)),i=await this.git(c,[`ls-files`,`-z`,`--`,...n]);if(i===void 0)continue;let s=await this.git(c,[`status`,`--porcelain`,`-z`,`--untracked-files=no`,`--`,...n]);if(s===void 0)continue;let l=new Set(o(s));for(let t of a(i))l.has(t)||r.add(e.resolve(c,t))}return r}async topLevel(t){let n=(await this.git(t,[`rev-parse`,`--show-toplevel`]))?.trim();return n===void 0||n===``?void 0:e.resolve(n)}async git(e,t){try{let{stdout:n}=await r(`git`,[...t],{cwd:e,encoding:`utf8`,maxBuffer:4194304});return n}catch{return}}};export{c as NodeGitStatusAdapter};
2
+ //# sourceMappingURL=gitStatus.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitStatus.mjs","names":[],"sources":["../../../src/adapters/node/gitStatus.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport type { GitStatusPort } from '../../types/gitStatus.ts';\n\n/**\n * Answers the unchanged question with git, in two batched calls per chunk.\n *\n * One technology per adapter. This is where git enters the scan path; the\n * tracker that depends on the capability keeps importing the port.\n *\n * `ls-files` names which of the given paths git tracks at all, and `status`\n * names which of those moved. Everything else, an ignored build artefact, a\n * temporary file, a tree with no repository, falls out of both answers and is\n * therefore never reported as unchanged: the caller must keep recording it,\n * because git has nothing to say about a file it never saw.\n */\n\n/** Paths per git invocation, so a large scan cannot overflow the argument list. */\nconst CHUNK = 200;\nconst MAX_BUFFER = 4 * 1024 * 1024;\n\nconst run = promisify(execFile);\n\nfunction chunked<T>(values: readonly T[], size: number): T[][] {\n const groups: T[][] = [];\n for (let index = 0; index < values.length; index += size) groups.push(values.slice(index, index + size));\n return groups;\n}\n\nfunction splitRecords(output: string): string[] {\n return output.split('\\0').filter((entry) => entry !== '');\n}\n\n/**\n * The paths named by `status --porcelain -z`, whose records carry a two-letter\n * code and a space. A rename adds a bare second record; keeping it is harmless,\n * because every path in this set is one the caller records anyway.\n */\nfunction statusPaths(output: string): string[] {\n return splitRecords(output).map((record) => (record.length > 3 && record[2] === ' ' ? record.slice(3) : record));\n}\n\nfunction isInside(root: string, filePath: string): boolean {\n const relative = path.relative(root, filePath);\n return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nexport class NodeGitStatusAdapter implements GitStatusPort {\n async unchanged(cwd: string, filePaths: readonly string[]): Promise<ReadonlySet<string>> {\n const clean = new Set<string>();\n if (filePaths.length === 0) return clean;\n const root = await this.topLevel(cwd);\n if (root === undefined) return clean;\n\n for (const group of chunked(\n filePaths.filter((filePath) => isInside(root, filePath)),\n CHUNK,\n )) {\n const relative = group.map((filePath) => path.relative(root, filePath));\n const tracked = await this.git(root, ['ls-files', '-z', '--', ...relative]);\n if (tracked === undefined) continue;\n const dirty = await this.git(root, ['status', '--porcelain', '-z', '--untracked-files=no', '--', ...relative]);\n if (dirty === undefined) continue;\n const moved = new Set(statusPaths(dirty));\n for (const entry of splitRecords(tracked)) {\n if (!moved.has(entry)) clean.add(path.resolve(root, entry));\n }\n }\n return clean;\n }\n\n /** The repository root, or undefined when this tree is not one. */\n private async topLevel(cwd: string): Promise<string | undefined> {\n const output = await this.git(cwd, ['rev-parse', '--show-toplevel']);\n const root = output?.trim();\n return root === undefined || root === '' ? undefined : path.resolve(root);\n }\n\n /** Git's stdout, or undefined when the call failed for any reason at all. */\n private async git(cwd: string, args: readonly string[]): Promise<string | undefined> {\n try {\n const { stdout } = await run('git', [...args], { cwd, encoding: 'utf8', maxBuffer: MAX_BUFFER });\n return stdout;\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"6GAmBA,MAGM,EAAM,EAAU,CAAQ,EAE9B,SAAS,EAAW,EAAsB,EAAqB,CAC7D,IAAM,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAAM,EAAO,KAAK,EAAO,MAAM,EAAO,EAAQ,CAAI,CAAC,EACvG,OAAO,CACT,CAEA,SAAS,EAAa,EAA0B,CAC9C,OAAO,EAAO,MAAM,IAAI,CAAC,CAAC,OAAQ,GAAU,IAAU,EAAE,CAC1D,CAOA,SAAS,EAAY,EAA0B,CAC7C,OAAO,EAAa,CAAM,CAAC,CAAC,IAAK,GAAY,EAAO,OAAS,GAAK,EAAO,KAAO,IAAM,EAAO,MAAM,CAAC,EAAI,CAAO,CACjH,CAEA,SAAS,EAAS,EAAc,EAA2B,CACzD,IAAM,EAAW,EAAK,SAAS,EAAM,CAAQ,EAC7C,OAAO,IAAa,IAAM,CAAC,EAAS,WAAW,IAAI,GAAK,CAAC,EAAK,WAAW,CAAQ,CACnF,CAEA,IAAa,EAAb,KAA2D,CACzD,MAAM,UAAU,EAAa,EAA4D,CACvF,IAAM,EAAQ,IAAI,IAClB,GAAI,EAAU,SAAW,EAAG,OAAO,EACnC,IAAM,EAAO,MAAM,KAAK,SAAS,CAAG,EACpC,GAAI,IAAS,IAAA,GAAW,OAAO,EAE/B,IAAK,IAAM,KAAS,EAClB,EAAU,OAAQ,GAAa,EAAS,EAAM,CAAQ,CAAC,EACvD,GACF,EAAG,CACD,IAAM,EAAW,EAAM,IAAK,GAAa,EAAK,SAAS,EAAM,CAAQ,CAAC,EAChE,EAAU,MAAM,KAAK,IAAI,EAAM,CAAC,WAAY,KAAM,KAAM,GAAG,CAAQ,CAAC,EAC1E,GAAI,IAAY,IAAA,GAAW,SAC3B,IAAM,EAAQ,MAAM,KAAK,IAAI,EAAM,CAAC,SAAU,cAAe,KAAM,uBAAwB,KAAM,GAAG,CAAQ,CAAC,EAC7G,GAAI,IAAU,IAAA,GAAW,SACzB,IAAM,EAAQ,IAAI,IAAI,EAAY,CAAK,CAAC,EACxC,IAAK,IAAM,KAAS,EAAa,CAAO,EACjC,EAAM,IAAI,CAAK,GAAG,EAAM,IAAI,EAAK,QAAQ,EAAM,CAAK,CAAC,CAE9D,CACA,OAAO,CACT,CAGA,MAAc,SAAS,EAA0C,CAE/D,IAAM,GAAO,MADQ,KAAK,IAAI,EAAK,CAAC,YAAa,iBAAiB,CAAC,EAAA,EAC9C,KAAK,EAC1B,OAAO,IAAS,IAAA,IAAa,IAAS,GAAK,IAAA,GAAY,EAAK,QAAQ,CAAI,CAC1E,CAGA,MAAc,IAAI,EAAa,EAAsD,CACnF,GAAI,CACF,GAAM,CAAE,UAAW,MAAM,EAAI,MAAO,CAAC,GAAG,CAAI,EAAG,CAAE,MAAK,SAAU,OAAQ,UAAW,OAAW,CAAC,EAC/F,OAAO,CACT,MAAQ,CACN,MACF,CACF,CACF"}
@@ -1,2 +1,2 @@
1
- const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./FileEditPaths/FileEditPaths.cjs"),n=require("../services/fileChanges.cjs"),r=require("../types/webFiles.cjs"),i=require("../services/doomIgnore.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs");o=e.__toESM(o,1);function s(e){try{return o.default.statSync(e).isFile()}catch{return!1}}function c(e,t){let r;try{r=o.default.readFileSync(e,`utf8`)}catch{return[]}let c=n.parseTimeline(r),l=n.foldEntries(c).filter(e=>s(e.path)).map(e=>({path:e.path,relPath:a.default.relative(t,e.path)||e.path,tool:e.tool,at:e.at,count:e.count,diffable:n.isDiffable(n.foldVersions(c,e.path))}));try{let e=o.default.readFileSync(a.default.join(t,`.doomignore`),`utf8`);return i.filterDoomIgnoredFiles(l,e)}catch{return l}}function l(e,n,r={}){let i=new t.FileEditPaths,s;try{s=i.timelinePath(e.cwd,i.sessionKey(e.sessionId))}catch{return n([]),{close:()=>void 0}}let l=!1,u,d,f,p=()=>{if(l)return;let t=c(s,e.cwd),r=JSON.stringify(t);r!==f&&(f=r,n(t))},m=()=>{if(!(u||l))try{u=o.default.watch(a.default.dirname(s),()=>{d&&clearTimeout(d),d=setTimeout(p,r.debounceMs??120)}),u.on(`error`,()=>{u?.close(),u=void 0})}catch{}};m(),p();let h=setInterval(()=>{m(),p()},r.pollMs??2e3);return{close(){l=!0,d&&clearTimeout(d),clearInterval(h),u?.close(),u=void 0}}}function u(e=l){return{frameType:r.filesChannelType,start(t){let n=new Map,r=new Map;return{payloadFor(e){let t=n.get(e.sessionId);return t===void 0?void 0:{items:t}},sessionAdded(i){r.set(i.sessionId,e(i,e=>{n.set(i.sessionId,e),t.publish(i.sessionId,{items:e})}))},sessionRemoved(e){r.get(e)?.close(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e.close();r.clear(),n.clear()}}}}}const d=[u()];exports.createFilesChannel=u,exports.readSessionFiles=c,exports.watchFiles=l,exports.webHubChannels=d;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./FileEditPaths/FileEditPaths.cjs"),n=require("../services/fileChanges.cjs"),r=require("../types/webFiles.cjs"),i=require("../services/doomIgnore.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs");o=e.__toESM(o,1);function s(e){try{return o.default.statSync(e).isFile()}catch{return!1}}function c(e,t){let r;try{r=o.default.readFileSync(e,`utf8`)}catch{return[]}let c=n.confirmedChanges(n.parseTimeline(r)),l=n.foldEntries(c).filter(e=>s(e.path)).map(e=>({path:e.path,relPath:a.default.relative(t,e.path)||e.path,tool:e.tool,at:e.at,count:e.count,diffable:n.isDiffable(n.foldVersions(c,e.path))}));try{let e=o.default.readFileSync(a.default.join(t,`.doomignore`),`utf8`);return i.filterDoomIgnoredFiles(l,e)}catch{return l}}function l(e,n,r={}){let i=new t.FileEditPaths,s;try{s=i.timelinePath(e.cwd,i.sessionKey(e.sessionId))}catch{return n([]),{close:()=>void 0}}let l=!1,u,d,f,p=()=>{if(l)return;let t=c(s,e.cwd),r=JSON.stringify(t);r!==f&&(f=r,n(t))},m=()=>{if(!(u||l))try{u=o.default.watch(a.default.dirname(s),()=>{d&&clearTimeout(d),d=setTimeout(p,r.debounceMs??120)}),u.on(`error`,()=>{u?.close(),u=void 0})}catch{}};m(),p();let h=setInterval(()=>{m(),p()},r.pollMs??2e3);return{close(){l=!0,d&&clearTimeout(d),clearInterval(h),u?.close(),u=void 0}}}function u(e=l){return{frameType:r.filesChannelType,start(t){let n=new Map,r=new Map;return{payloadFor(e){let t=n.get(e.sessionId);return t===void 0?void 0:{items:t}},sessionAdded(i){r.set(i.sessionId,e(i,e=>{n.set(i.sessionId,e),t.publish(i.sessionId,{items:e})}))},sessionRemoved(e){r.get(e)?.close(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e.close();r.clear(),n.clear()}}}}}const d=[u()];exports.createFilesChannel=u,exports.readSessionFiles=c,exports.watchFiles=l,exports.webHubChannels=d;
2
2
  //# sourceMappingURL=webFilesChannel.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"webFilesChannel.cjs","names":["fs","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","FileEditPaths","filesChannelType"],"sources":["../../src/adapters/webFilesChannel.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { filterDoomIgnoredFiles } from '../services/doomIgnore.ts';\nimport { foldEntries, foldVersions, isDiffable, parseTimeline } from '../services/fileChanges.ts';\nimport { filesChannelType, type FilesItemView } from '../types/webFiles.ts';\nimport { FileEditPaths } from './FileEditPaths/FileEditPaths.ts';\n\n/**\n * The changed-files channel: one watcher per managed session over the timeline\n * that session's extension appends to.\n *\n * The hub runs in its own process, so it reads what the extension wrote rather\n * than talking to it. Both halves derive the same path from the same session id\n * and working directory, which is what lets them meet on disk without an\n * agreement to keep in sync.\n *\n * Same reliability posture as this repository's other watchers: the poll is the\n * source of truth and fs.watch is only an accelerator, so a missed inotify\n * event costs latency rather than correctness, and a timeline that does not\n * exist yet (a session that has changed nothing) is a normal state.\n */\n\nconst POLL_MS = 2000;\nconst DEBOUNCE_MS = 120;\n\nexport interface FilesSource {\n close(): void;\n}\n\nexport interface WatchFilesOptions {\n pollMs?: number;\n debounceMs?: number;\n}\n\n/** Whether the file is still there to be opened. */\nfunction stillExists(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Reads one session's timeline and presents it as the rows the dock lists.\n *\n * A file the session changed and then removed is left out. The dock is a list\n * of things to open, and a row that can only ever answer \"this is gone\" is a\n * dead end; the timeline still holds the change, so a tab already open on the\n * file keeps working and says so.\n */\nexport function readSessionFiles(timelinePath: string, cwd: string): FilesItemView[] {\n let content: string;\n try {\n content = fs.readFileSync(timelinePath, 'utf8');\n } catch {\n return [];\n }\n const events = parseTimeline(content);\n const items = foldEntries(events)\n .filter((entry) => stillExists(entry.path))\n .map((entry) => ({\n path: entry.path,\n relPath: path.relative(cwd, entry.path) || entry.path,\n tool: entry.tool,\n at: entry.at,\n count: entry.count,\n diffable: isDiffable(foldVersions(events, entry.path)),\n }));\n\n try {\n const doomIgnore = fs.readFileSync(path.join(cwd, '.doomignore'), 'utf8');\n return filterDoomIgnoredFiles(items, doomIgnore);\n } catch {\n return items;\n }\n}\n\n/**\n * Watches one session's timeline and reports every change to its file list.\n * Announcements are deduped by fingerprint, so an unchanged list is silent.\n */\nexport function watchFiles(\n scope: HubSessionScope,\n onChange: (items: FilesItemView[]) => void,\n options: WatchFilesOptions = {},\n): FilesSource {\n const paths = new FileEditPaths();\n let timelinePath: string;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // A working directory that cannot be resolved has no timeline to read; the\n // session simply reports nothing rather than taking the hub down with it.\n onChange([]);\n return { close: () => undefined };\n }\n\n let closed = false;\n let watcher: fs.FSWatcher | undefined;\n let debounce: NodeJS.Timeout | undefined;\n let lastEmitted: string | undefined;\n\n const emit = (): void => {\n if (closed) return;\n const items = readSessionFiles(timelinePath, scope.cwd);\n const fingerprint = JSON.stringify(items);\n if (fingerprint === lastEmitted) return;\n lastEmitted = fingerprint;\n onChange(items);\n };\n\n // The timeline file is created on the first change, so the watch is placed on\n // the directory that holds it and re-checked by the poll until it appears.\n const ensureWatcher = (): void => {\n if (watcher || closed) return;\n try {\n watcher = fs.watch(path.dirname(timelinePath), () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(emit, options.debounceMs ?? DEBOUNCE_MS);\n });\n watcher.on('error', () => {\n watcher?.close();\n watcher = undefined; // The poll re-establishes it once the directory is back.\n });\n } catch {\n // The directory does not exist yet; the poll keeps trying.\n }\n };\n\n ensureWatcher();\n emit();\n const poll = setInterval(() => {\n ensureWatcher();\n emit();\n }, options.pollMs ?? POLL_MS);\n\n return {\n close() {\n closed = true;\n if (debounce) clearTimeout(debounce);\n clearInterval(poll);\n watcher?.close();\n watcher = undefined;\n },\n };\n}\n\n/**\n * The files data channel: one watcher per managed session, published as\n * { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(watch: typeof watchFiles = watchFiles): WebHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const sources = new Map<string, FilesSource>();\n const channelSource: HubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n sources.set(\n scope.sessionId,\n watch(scope, (items) => {\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n }),\n );\n },\n sessionRemoved(sessionId) {\n sources.get(sessionId)?.close();\n sources.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const source of sources.values()) source.close();\n sources.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n\n/** The named export the hub imports from the built entry the doompiWeb block names. */\nexport const webHubChannels: readonly WebHubChannel[] = [createFilesChannel()];\n"],"mappings":"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"}
1
+ {"version":3,"file":"webFilesChannel.cjs","names":["fs","confirmedChanges","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","FileEditPaths","filesChannelType"],"sources":["../../src/adapters/webFilesChannel.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { filterDoomIgnoredFiles } from '../services/doomIgnore.ts';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../services/fileChanges.ts';\nimport { filesChannelType, type FilesItemView } from '../types/webFiles.ts';\nimport { FileEditPaths } from './FileEditPaths/FileEditPaths.ts';\n\n/**\n * The changed-files channel: one watcher per managed session over the timeline\n * that session's extension appends to.\n *\n * The hub runs in its own process, so it reads what the extension wrote rather\n * than talking to it. Both halves derive the same path from the same session id\n * and working directory, which is what lets them meet on disk without an\n * agreement to keep in sync.\n *\n * Same reliability posture as this repository's other watchers: the poll is the\n * source of truth and fs.watch is only an accelerator, so a missed inotify\n * event costs latency rather than correctness, and a timeline that does not\n * exist yet (a session that has changed nothing) is a normal state.\n */\n\nconst POLL_MS = 2000;\nconst DEBOUNCE_MS = 120;\n\nexport interface FilesSource {\n close(): void;\n}\n\nexport interface WatchFilesOptions {\n pollMs?: number;\n debounceMs?: number;\n}\n\n/** Whether the file is still there to be opened. */\nfunction stillExists(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Reads one session's timeline and presents it as the rows the dock lists.\n *\n * A file the session changed and then removed is left out, and so is a path a\n * bash call only touched: a command that moves a modification time without\n * changing a byte is not an edit, and listing it buries the ones that are. The\n * dock is a list of things to open, and a row that can only ever answer \"this\n * is gone\" is a dead end; the timeline still holds the change, so a tab already\n * open on the file keeps working and says so.\n */\nexport function readSessionFiles(timelinePath: string, cwd: string): FilesItemView[] {\n let content: string;\n try {\n content = fs.readFileSync(timelinePath, 'utf8');\n } catch {\n return [];\n }\n const events = confirmedChanges(parseTimeline(content));\n const items = foldEntries(events)\n .filter((entry) => stillExists(entry.path))\n .map((entry) => ({\n path: entry.path,\n relPath: path.relative(cwd, entry.path) || entry.path,\n tool: entry.tool,\n at: entry.at,\n count: entry.count,\n diffable: isDiffable(foldVersions(events, entry.path)),\n }));\n\n try {\n const doomIgnore = fs.readFileSync(path.join(cwd, '.doomignore'), 'utf8');\n return filterDoomIgnoredFiles(items, doomIgnore);\n } catch {\n return items;\n }\n}\n\n/**\n * Watches one session's timeline and reports every change to its file list.\n * Announcements are deduped by fingerprint, so an unchanged list is silent.\n */\nexport function watchFiles(\n scope: HubSessionScope,\n onChange: (items: FilesItemView[]) => void,\n options: WatchFilesOptions = {},\n): FilesSource {\n const paths = new FileEditPaths();\n let timelinePath: string;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // A working directory that cannot be resolved has no timeline to read; the\n // session simply reports nothing rather than taking the hub down with it.\n onChange([]);\n return { close: () => undefined };\n }\n\n let closed = false;\n let watcher: fs.FSWatcher | undefined;\n let debounce: NodeJS.Timeout | undefined;\n let lastEmitted: string | undefined;\n\n const emit = (): void => {\n if (closed) return;\n const items = readSessionFiles(timelinePath, scope.cwd);\n const fingerprint = JSON.stringify(items);\n if (fingerprint === lastEmitted) return;\n lastEmitted = fingerprint;\n onChange(items);\n };\n\n // The timeline file is created on the first change, so the watch is placed on\n // the directory that holds it and re-checked by the poll until it appears.\n const ensureWatcher = (): void => {\n if (watcher || closed) return;\n try {\n watcher = fs.watch(path.dirname(timelinePath), () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(emit, options.debounceMs ?? DEBOUNCE_MS);\n });\n watcher.on('error', () => {\n watcher?.close();\n watcher = undefined; // The poll re-establishes it once the directory is back.\n });\n } catch {\n // The directory does not exist yet; the poll keeps trying.\n }\n };\n\n ensureWatcher();\n emit();\n const poll = setInterval(() => {\n ensureWatcher();\n emit();\n }, options.pollMs ?? POLL_MS);\n\n return {\n close() {\n closed = true;\n if (debounce) clearTimeout(debounce);\n clearInterval(poll);\n watcher?.close();\n watcher = undefined;\n },\n };\n}\n\n/**\n * The files data channel: one watcher per managed session, published as\n * { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(watch: typeof watchFiles = watchFiles): WebHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const sources = new Map<string, FilesSource>();\n const channelSource: HubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n sources.set(\n scope.sessionId,\n watch(scope, (items) => {\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n }),\n );\n },\n sessionRemoved(sessionId) {\n sources.get(sessionId)?.close();\n sources.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const source of sources.values()) source.close();\n sources.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n\n/** The named export the hub imports from the built entry the doompiWeb block names. */\nexport const webHubChannels: readonly WebHubChannel[] = [createFilesChannel()];\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"webFilesChannel.d.cts","names":[],"sources":["../../src/adapters/webFilesChannel.ts"],"mappings":";;;UA0BiB;EACf;;UAGe;EACf;EACA;;;;;;iBAmDc,WACd,OAAO,iBACP,WAAW,OAAO,0BAClB,UAAS,oBACR;;;;;iBAkEa,mBAAmB,eAAc,aAA0B;;cAqC9D,yBAAyB"}
1
+ {"version":3,"file":"webFilesChannel.d.cts","names":[],"sources":["../../src/adapters/webFilesChannel.ts"],"mappings":";;;UA0BiB;EACf;;UAGe;EACf;EACA;;;;;;iBAqDc,WACd,OAAO,iBACP,WAAW,OAAO,0BAClB,UAAS,oBACR;;;;;iBAkEa,mBAAmB,eAAc,aAA0B;;cAqC9D,yBAAyB"}
@@ -1 +1 @@
1
- {"version":3,"file":"webFilesChannel.d.mts","names":[],"sources":["../../src/adapters/webFilesChannel.ts"],"mappings":";;;UA0BiB;EACf;;UAGe;EACf;EACA;;;;;;iBAmDc,WACd,OAAO,iBACP,WAAW,OAAO,0BAClB,UAAS,oBACR;;;;;iBAkEa,mBAAmB,eAAc,aAA0B;;cAqC9D,yBAAyB"}
1
+ {"version":3,"file":"webFilesChannel.d.mts","names":[],"sources":["../../src/adapters/webFilesChannel.ts"],"mappings":";;;UA0BiB;EACf;;UAGe;EACf;EACA;;;;;;iBAqDc,WACd,OAAO,iBACP,WAAW,OAAO,0BAClB,UAAS,oBACR;;;;;iBAkEa,mBAAmB,eAAc,aAA0B;;cAqC9D,yBAAyB"}
@@ -1,2 +1,2 @@
1
- import{FileEditPaths as e}from"./FileEditPaths/FileEditPaths.mjs";import{foldEntries as t,foldVersions as n,isDiffable as r,parseTimeline as i}from"../services/fileChanges.mjs";import{filesChannelType as a}from"../types/webFiles.mjs";import{filterDoomIgnoredFiles as o}from"../services/doomIgnore.mjs";import s from"node:path";import c from"node:fs";function l(e){try{return c.statSync(e).isFile()}catch{return!1}}function u(e,a){let u;try{u=c.readFileSync(e,`utf8`)}catch{return[]}let d=i(u),f=t(d).filter(e=>l(e.path)).map(e=>({path:e.path,relPath:s.relative(a,e.path)||e.path,tool:e.tool,at:e.at,count:e.count,diffable:r(n(d,e.path))}));try{let e=c.readFileSync(s.join(a,`.doomignore`),`utf8`);return o(f,e)}catch{return f}}function d(t,n,r={}){let i=new e,a;try{a=i.timelinePath(t.cwd,i.sessionKey(t.sessionId))}catch{return n([]),{close:()=>void 0}}let o=!1,l,d,f,p=()=>{if(o)return;let e=u(a,t.cwd),r=JSON.stringify(e);r!==f&&(f=r,n(e))},m=()=>{if(!(l||o))try{l=c.watch(s.dirname(a),()=>{d&&clearTimeout(d),d=setTimeout(p,r.debounceMs??120)}),l.on(`error`,()=>{l?.close(),l=void 0})}catch{}};m(),p();let h=setInterval(()=>{m(),p()},r.pollMs??2e3);return{close(){o=!0,d&&clearTimeout(d),clearInterval(h),l?.close(),l=void 0}}}function f(e=d){return{frameType:a,start(t){let n=new Map,r=new Map;return{payloadFor(e){let t=n.get(e.sessionId);return t===void 0?void 0:{items:t}},sessionAdded(i){r.set(i.sessionId,e(i,e=>{n.set(i.sessionId,e),t.publish(i.sessionId,{items:e})}))},sessionRemoved(e){r.get(e)?.close(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e.close();r.clear(),n.clear()}}}}}const p=[f()];export{f as createFilesChannel,u as readSessionFiles,d as watchFiles,p as webHubChannels};
1
+ import{FileEditPaths as e}from"./FileEditPaths/FileEditPaths.mjs";import{confirmedChanges as t,foldEntries as n,foldVersions as r,isDiffable as i,parseTimeline as a}from"../services/fileChanges.mjs";import{filesChannelType as o}from"../types/webFiles.mjs";import{filterDoomIgnoredFiles as s}from"../services/doomIgnore.mjs";import c from"node:path";import l from"node:fs";function u(e){try{return l.statSync(e).isFile()}catch{return!1}}function d(e,o){let d;try{d=l.readFileSync(e,`utf8`)}catch{return[]}let f=t(a(d)),p=n(f).filter(e=>u(e.path)).map(e=>({path:e.path,relPath:c.relative(o,e.path)||e.path,tool:e.tool,at:e.at,count:e.count,diffable:i(r(f,e.path))}));try{let e=l.readFileSync(c.join(o,`.doomignore`),`utf8`);return s(p,e)}catch{return p}}function f(t,n,r={}){let i=new e,a;try{a=i.timelinePath(t.cwd,i.sessionKey(t.sessionId))}catch{return n([]),{close:()=>void 0}}let o=!1,s,u,f,p=()=>{if(o)return;let e=d(a,t.cwd),r=JSON.stringify(e);r!==f&&(f=r,n(e))},m=()=>{if(!(s||o))try{s=l.watch(c.dirname(a),()=>{u&&clearTimeout(u),u=setTimeout(p,r.debounceMs??120)}),s.on(`error`,()=>{s?.close(),s=void 0})}catch{}};m(),p();let h=setInterval(()=>{m(),p()},r.pollMs??2e3);return{close(){o=!0,u&&clearTimeout(u),clearInterval(h),s?.close(),s=void 0}}}function p(e=f){return{frameType:o,start(t){let n=new Map,r=new Map;return{payloadFor(e){let t=n.get(e.sessionId);return t===void 0?void 0:{items:t}},sessionAdded(i){r.set(i.sessionId,e(i,e=>{n.set(i.sessionId,e),t.publish(i.sessionId,{items:e})}))},sessionRemoved(e){r.get(e)?.close(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e.close();r.clear(),n.clear()}}}}}const m=[p()];export{p as createFilesChannel,d as readSessionFiles,f as watchFiles,m as webHubChannels};
2
2
  //# sourceMappingURL=webFilesChannel.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"webFilesChannel.mjs","names":[],"sources":["../../src/adapters/webFilesChannel.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { filterDoomIgnoredFiles } from '../services/doomIgnore.ts';\nimport { foldEntries, foldVersions, isDiffable, parseTimeline } from '../services/fileChanges.ts';\nimport { filesChannelType, type FilesItemView } from '../types/webFiles.ts';\nimport { FileEditPaths } from './FileEditPaths/FileEditPaths.ts';\n\n/**\n * The changed-files channel: one watcher per managed session over the timeline\n * that session's extension appends to.\n *\n * The hub runs in its own process, so it reads what the extension wrote rather\n * than talking to it. Both halves derive the same path from the same session id\n * and working directory, which is what lets them meet on disk without an\n * agreement to keep in sync.\n *\n * Same reliability posture as this repository's other watchers: the poll is the\n * source of truth and fs.watch is only an accelerator, so a missed inotify\n * event costs latency rather than correctness, and a timeline that does not\n * exist yet (a session that has changed nothing) is a normal state.\n */\n\nconst POLL_MS = 2000;\nconst DEBOUNCE_MS = 120;\n\nexport interface FilesSource {\n close(): void;\n}\n\nexport interface WatchFilesOptions {\n pollMs?: number;\n debounceMs?: number;\n}\n\n/** Whether the file is still there to be opened. */\nfunction stillExists(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Reads one session's timeline and presents it as the rows the dock lists.\n *\n * A file the session changed and then removed is left out. The dock is a list\n * of things to open, and a row that can only ever answer \"this is gone\" is a\n * dead end; the timeline still holds the change, so a tab already open on the\n * file keeps working and says so.\n */\nexport function readSessionFiles(timelinePath: string, cwd: string): FilesItemView[] {\n let content: string;\n try {\n content = fs.readFileSync(timelinePath, 'utf8');\n } catch {\n return [];\n }\n const events = parseTimeline(content);\n const items = foldEntries(events)\n .filter((entry) => stillExists(entry.path))\n .map((entry) => ({\n path: entry.path,\n relPath: path.relative(cwd, entry.path) || entry.path,\n tool: entry.tool,\n at: entry.at,\n count: entry.count,\n diffable: isDiffable(foldVersions(events, entry.path)),\n }));\n\n try {\n const doomIgnore = fs.readFileSync(path.join(cwd, '.doomignore'), 'utf8');\n return filterDoomIgnoredFiles(items, doomIgnore);\n } catch {\n return items;\n }\n}\n\n/**\n * Watches one session's timeline and reports every change to its file list.\n * Announcements are deduped by fingerprint, so an unchanged list is silent.\n */\nexport function watchFiles(\n scope: HubSessionScope,\n onChange: (items: FilesItemView[]) => void,\n options: WatchFilesOptions = {},\n): FilesSource {\n const paths = new FileEditPaths();\n let timelinePath: string;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // A working directory that cannot be resolved has no timeline to read; the\n // session simply reports nothing rather than taking the hub down with it.\n onChange([]);\n return { close: () => undefined };\n }\n\n let closed = false;\n let watcher: fs.FSWatcher | undefined;\n let debounce: NodeJS.Timeout | undefined;\n let lastEmitted: string | undefined;\n\n const emit = (): void => {\n if (closed) return;\n const items = readSessionFiles(timelinePath, scope.cwd);\n const fingerprint = JSON.stringify(items);\n if (fingerprint === lastEmitted) return;\n lastEmitted = fingerprint;\n onChange(items);\n };\n\n // The timeline file is created on the first change, so the watch is placed on\n // the directory that holds it and re-checked by the poll until it appears.\n const ensureWatcher = (): void => {\n if (watcher || closed) return;\n try {\n watcher = fs.watch(path.dirname(timelinePath), () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(emit, options.debounceMs ?? DEBOUNCE_MS);\n });\n watcher.on('error', () => {\n watcher?.close();\n watcher = undefined; // The poll re-establishes it once the directory is back.\n });\n } catch {\n // The directory does not exist yet; the poll keeps trying.\n }\n };\n\n ensureWatcher();\n emit();\n const poll = setInterval(() => {\n ensureWatcher();\n emit();\n }, options.pollMs ?? POLL_MS);\n\n return {\n close() {\n closed = true;\n if (debounce) clearTimeout(debounce);\n clearInterval(poll);\n watcher?.close();\n watcher = undefined;\n },\n };\n}\n\n/**\n * The files data channel: one watcher per managed session, published as\n * { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(watch: typeof watchFiles = watchFiles): WebHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const sources = new Map<string, FilesSource>();\n const channelSource: HubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n sources.set(\n scope.sessionId,\n watch(scope, (items) => {\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n }),\n );\n },\n sessionRemoved(sessionId) {\n sources.get(sessionId)?.close();\n sources.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const source of sources.values()) source.close();\n sources.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n\n/** The named export the hub imports from the built entry the doompiWeb block names. */\nexport const webHubChannels: readonly WebHubChannel[] = [createFilesChannel()];\n"],"mappings":"8VAoCA,SAAS,EAAY,EAA2B,CAC9C,GAAI,CACF,OAAO,EAAG,SAAS,CAAQ,CAAC,CAAC,OAAO,CACtC,MAAQ,CACN,MAAO,EACT,CACF,CAUA,SAAgB,EAAiB,EAAsB,EAA8B,CACnF,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,aAAa,EAAc,MAAM,CAChD,MAAQ,CACN,MAAO,CAAC,CACV,CACA,IAAM,EAAS,EAAc,CAAO,EAC9B,EAAQ,EAAY,CAAM,CAAC,CAC9B,OAAQ,GAAU,EAAY,EAAM,IAAI,CAAC,CAAC,CAC1C,IAAK,IAAW,CACf,KAAM,EAAM,KACZ,QAAS,EAAK,SAAS,EAAK,EAAM,IAAI,GAAK,EAAM,KACjD,KAAM,EAAM,KACZ,GAAI,EAAM,GACV,MAAO,EAAM,MACb,SAAU,EAAW,EAAa,EAAQ,EAAM,IAAI,CAAC,CACvD,EAAE,EAEJ,GAAI,CACF,IAAM,EAAa,EAAG,aAAa,EAAK,KAAK,EAAK,aAAa,EAAG,MAAM,EACxE,OAAO,EAAuB,EAAO,CAAU,CACjD,MAAQ,CACN,OAAO,CACT,CACF,CAMA,SAAgB,EACd,EACA,EACA,EAA6B,CAAC,EACjB,CACb,IAAM,EAAQ,IAAI,EACd,EACJ,GAAI,CACF,EAAe,EAAM,aAAa,EAAM,IAAK,EAAM,WAAW,EAAM,SAAS,CAAC,CAChF,MAAQ,CAIN,OADA,EAAS,CAAC,CAAC,EACJ,CAAE,UAAa,IAAA,EAAU,CAClC,CAEA,IAAI,EAAS,GACT,EACA,EACA,EAEE,MAAmB,CACvB,GAAI,EAAQ,OACZ,IAAM,EAAQ,EAAiB,EAAc,EAAM,GAAG,EAChD,EAAc,KAAK,UAAU,CAAK,EACpC,IAAgB,IACpB,EAAc,EACd,EAAS,CAAK,EAChB,EAIM,MAA4B,CAC5B,QAAW,GACf,GAAI,CACF,EAAU,EAAG,MAAM,EAAK,QAAQ,CAAY,MAAS,CAC/C,GAAU,aAAa,CAAQ,EACnC,EAAW,WAAW,EAAM,EAAQ,YAAc,GAAW,CAC/D,CAAC,EACD,EAAQ,GAAG,YAAe,CACxB,GAAS,MAAM,EACf,EAAU,IAAA,EACZ,CAAC,CACH,MAAQ,CAER,CACF,EAEA,EAAc,EACd,EAAK,EACL,IAAM,EAAO,gBAAkB,CAC7B,EAAc,EACd,EAAK,CACP,EAAG,EAAQ,QAAU,GAAO,EAE5B,MAAO,CACL,OAAQ,CACN,EAAS,GACL,GAAU,aAAa,CAAQ,EACnC,cAAc,CAAI,EAClB,GAAS,MAAM,EACf,EAAU,IAAA,EACZ,CACF,CACF,CAMA,SAAgB,EAAmB,EAA2B,EAA2B,CACvF,MAAO,CACL,UAAW,EACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAU,IAAI,IA0BpB,MAAO,CAxBL,WAAW,EAAO,CAChB,IAAM,EAAQ,EAAO,IAAI,EAAM,SAAS,EACxC,OAAO,IAAU,IAAA,GAAY,IAAA,GAAY,CAAE,OAAM,CACnD,EACA,aAAa,EAAO,CAClB,EAAQ,IACN,EAAM,UACN,EAAM,EAAQ,GAAU,CACtB,EAAO,IAAI,EAAM,UAAW,CAAK,EACjC,EAAK,QAAQ,EAAM,UAAW,CAAE,OAAM,CAAC,CACzC,CAAC,CACH,CACF,EACA,eAAe,EAAW,CACxB,EAAQ,IAAI,CAAS,CAAC,EAAE,MAAM,EAC9B,EAAQ,OAAO,CAAS,EACxB,EAAO,OAAO,CAAS,CACzB,EACA,OAAQ,CACN,IAAK,IAAM,KAAU,EAAQ,OAAO,EAAG,EAAO,MAAM,EACpD,EAAQ,MAAM,EACd,EAAO,MAAM,CACf,CAEiB,CACrB,CACF,CACF,CAGA,MAAa,EAA2C,CAAC,EAAmB,CAAC"}
1
+ {"version":3,"file":"webFilesChannel.mjs","names":[],"sources":["../../src/adapters/webFilesChannel.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { filterDoomIgnoredFiles } from '../services/doomIgnore.ts';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../services/fileChanges.ts';\nimport { filesChannelType, type FilesItemView } from '../types/webFiles.ts';\nimport { FileEditPaths } from './FileEditPaths/FileEditPaths.ts';\n\n/**\n * The changed-files channel: one watcher per managed session over the timeline\n * that session's extension appends to.\n *\n * The hub runs in its own process, so it reads what the extension wrote rather\n * than talking to it. Both halves derive the same path from the same session id\n * and working directory, which is what lets them meet on disk without an\n * agreement to keep in sync.\n *\n * Same reliability posture as this repository's other watchers: the poll is the\n * source of truth and fs.watch is only an accelerator, so a missed inotify\n * event costs latency rather than correctness, and a timeline that does not\n * exist yet (a session that has changed nothing) is a normal state.\n */\n\nconst POLL_MS = 2000;\nconst DEBOUNCE_MS = 120;\n\nexport interface FilesSource {\n close(): void;\n}\n\nexport interface WatchFilesOptions {\n pollMs?: number;\n debounceMs?: number;\n}\n\n/** Whether the file is still there to be opened. */\nfunction stillExists(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Reads one session's timeline and presents it as the rows the dock lists.\n *\n * A file the session changed and then removed is left out, and so is a path a\n * bash call only touched: a command that moves a modification time without\n * changing a byte is not an edit, and listing it buries the ones that are. The\n * dock is a list of things to open, and a row that can only ever answer \"this\n * is gone\" is a dead end; the timeline still holds the change, so a tab already\n * open on the file keeps working and says so.\n */\nexport function readSessionFiles(timelinePath: string, cwd: string): FilesItemView[] {\n let content: string;\n try {\n content = fs.readFileSync(timelinePath, 'utf8');\n } catch {\n return [];\n }\n const events = confirmedChanges(parseTimeline(content));\n const items = foldEntries(events)\n .filter((entry) => stillExists(entry.path))\n .map((entry) => ({\n path: entry.path,\n relPath: path.relative(cwd, entry.path) || entry.path,\n tool: entry.tool,\n at: entry.at,\n count: entry.count,\n diffable: isDiffable(foldVersions(events, entry.path)),\n }));\n\n try {\n const doomIgnore = fs.readFileSync(path.join(cwd, '.doomignore'), 'utf8');\n return filterDoomIgnoredFiles(items, doomIgnore);\n } catch {\n return items;\n }\n}\n\n/**\n * Watches one session's timeline and reports every change to its file list.\n * Announcements are deduped by fingerprint, so an unchanged list is silent.\n */\nexport function watchFiles(\n scope: HubSessionScope,\n onChange: (items: FilesItemView[]) => void,\n options: WatchFilesOptions = {},\n): FilesSource {\n const paths = new FileEditPaths();\n let timelinePath: string;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // A working directory that cannot be resolved has no timeline to read; the\n // session simply reports nothing rather than taking the hub down with it.\n onChange([]);\n return { close: () => undefined };\n }\n\n let closed = false;\n let watcher: fs.FSWatcher | undefined;\n let debounce: NodeJS.Timeout | undefined;\n let lastEmitted: string | undefined;\n\n const emit = (): void => {\n if (closed) return;\n const items = readSessionFiles(timelinePath, scope.cwd);\n const fingerprint = JSON.stringify(items);\n if (fingerprint === lastEmitted) return;\n lastEmitted = fingerprint;\n onChange(items);\n };\n\n // The timeline file is created on the first change, so the watch is placed on\n // the directory that holds it and re-checked by the poll until it appears.\n const ensureWatcher = (): void => {\n if (watcher || closed) return;\n try {\n watcher = fs.watch(path.dirname(timelinePath), () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(emit, options.debounceMs ?? DEBOUNCE_MS);\n });\n watcher.on('error', () => {\n watcher?.close();\n watcher = undefined; // The poll re-establishes it once the directory is back.\n });\n } catch {\n // The directory does not exist yet; the poll keeps trying.\n }\n };\n\n ensureWatcher();\n emit();\n const poll = setInterval(() => {\n ensureWatcher();\n emit();\n }, options.pollMs ?? POLL_MS);\n\n return {\n close() {\n closed = true;\n if (debounce) clearTimeout(debounce);\n clearInterval(poll);\n watcher?.close();\n watcher = undefined;\n },\n };\n}\n\n/**\n * The files data channel: one watcher per managed session, published as\n * { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(watch: typeof watchFiles = watchFiles): WebHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const sources = new Map<string, FilesSource>();\n const channelSource: HubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n sources.set(\n scope.sessionId,\n watch(scope, (items) => {\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n }),\n );\n },\n sessionRemoved(sessionId) {\n sources.get(sessionId)?.close();\n sources.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const source of sources.values()) source.close();\n sources.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n\n/** The named export the hub imports from the built entry the doompiWeb block names. */\nexport const webHubChannels: readonly WebHubChannel[] = [createFilesChannel()];\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../adapters/EditorConfigService/EditorConfigService.cjs"),t=require("../adapters/EditorLauncher/EditorLauncher.cjs"),n=require("../adapters/EditTracker/EditTracker.cjs"),r=require("../adapters/FileEditPaths/FileEditPaths.cjs"),i=require("../adapters/node/snapshotStore.cjs"),a=require("../adapters/node/treeManifest.cjs"),o=require("../tui/fileEditWorkflow.cjs"),s=require("../adapters/GitDiffService/GitDiffService.cjs"),c=require("../adapters/TimelineStore/TimelineStore.cjs");function l(l={}){let u=l.paths??new r.FileEditPaths,d=l.timeline??new c.TimelineStore,f=l.snapshots??new i.NodeSnapshotStoreAdapter,p=l.manifests??new a.NodeTreeManifestAdapter,m=l.diffs??new s.GitDiffService,h=l.editorConfig??new e.EditorConfigService,g=l.editTracker??new n.EditTracker(d,f,p),_=l.editorLauncher??new t.EditorLauncher(h);return{paths:u,timeline:d,snapshots:f,manifests:p,diffs:m,editorConfig:h,editTracker:g,editorLauncher:_,workflow:l.workflow??new o.FileEditWorkflow(d,m,h,_)}}exports.createFileEditContainer=l;
1
+ const e=require("../adapters/EditorConfigService/EditorConfigService.cjs"),t=require("../adapters/EditorLauncher/EditorLauncher.cjs"),n=require("../adapters/EditTracker/EditTracker.cjs"),r=require("../adapters/FileEditPaths/FileEditPaths.cjs"),i=require("../adapters/node/gitStatus.cjs"),a=require("../adapters/node/snapshotStore.cjs"),o=require("../adapters/node/treeManifest.cjs"),s=require("../tui/fileEditWorkflow.cjs"),c=require("../adapters/GitDiffService/GitDiffService.cjs"),l=require("../adapters/TimelineStore/TimelineStore.cjs");function u(u={}){let d=u.paths??new r.FileEditPaths,f=u.timeline??new l.TimelineStore,p=u.snapshots??new a.NodeSnapshotStoreAdapter,m=u.manifests??new o.NodeTreeManifestAdapter,h=u.diffs??new c.GitDiffService,g=u.editorConfig??new e.EditorConfigService,_=u.editTracker??new n.EditTracker(f,p,m,{git:new i.NodeGitStatusAdapter}),v=u.editorLauncher??new t.EditorLauncher(g);return{paths:d,timeline:f,snapshots:p,manifests:m,diffs:h,editorConfig:g,editTracker:_,editorLauncher:v,workflow:u.workflow??new s.FileEditWorkflow(f,h,g,v)}}exports.createFileEditContainer=u;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","NodeTreeManifestAdapter","GitDiffService","EditorConfigService","EditTracker","EditorLauncher","FileEditWorkflow"],"sources":["../../src/container/index.ts"],"sourcesContent":["import { EditorConfigService } from '../adapters/EditorConfigService/EditorConfigService';\nimport { EditorLauncher } from '../adapters/EditorLauncher/EditorLauncher';\nimport { EditTracker } from '../adapters/EditTracker/EditTracker';\nimport { FileEditPaths } from '../adapters/FileEditPaths/FileEditPaths';\nimport { NodeSnapshotStoreAdapter } from '../adapters/node/snapshotStore';\nimport { NodeTreeManifestAdapter } from '../adapters/node/treeManifest';\nimport { FileEditWorkflow } from '../tui/fileEditWorkflow';\nimport { GitDiffService } from '../adapters/GitDiffService/GitDiffService';\nimport { TimelineStore } from '../adapters/TimelineStore/TimelineStore';\nimport type { FileEditDependencies } from '../types';\n\n/**\n * Compose the file-edit runtime.\n *\n * Construction order is the dependency order, so the graph is readable top to\n * bottom and a cycle is a compile error rather than a resolution failure at\n * runtime. Pass overrides to substitute a double in tests.\n */\nexport function createFileEditContainer(overrides: Partial<FileEditDependencies> = {}): FileEditDependencies {\n const paths = overrides.paths ?? new FileEditPaths();\n const timeline = overrides.timeline ?? new TimelineStore();\n const snapshots = overrides.snapshots ?? new NodeSnapshotStoreAdapter();\n const manifests = overrides.manifests ?? new NodeTreeManifestAdapter();\n const diffs = overrides.diffs ?? new GitDiffService();\n const editorConfig = overrides.editorConfig ?? new EditorConfigService();\n const editTracker = overrides.editTracker ?? new EditTracker(timeline, snapshots, manifests);\n const editorLauncher = overrides.editorLauncher ?? new EditorLauncher(editorConfig);\n const workflow = overrides.workflow ?? new FileEditWorkflow(timeline, diffs, editorConfig, editorLauncher);\n\n return { paths, timeline, snapshots, manifests, diffs, editorConfig, editTracker, editorLauncher, workflow };\n}\n"],"mappings":"gfAkBA,SAAgB,EAAwB,EAA2C,CAAC,EAAyB,CAC3G,IAAM,EAAQ,EAAU,OAAS,IAAIA,EAAAA,cAC/B,EAAW,EAAU,UAAY,IAAIC,EAAAA,cACrC,EAAY,EAAU,WAAa,IAAIC,EAAAA,yBACvC,EAAY,EAAU,WAAa,IAAIC,EAAAA,wBACvC,EAAQ,EAAU,OAAS,IAAIC,EAAAA,eAC/B,EAAe,EAAU,cAAgB,IAAIC,EAAAA,oBAC7C,EAAc,EAAU,aAAe,IAAIC,EAAAA,YAAY,EAAU,EAAW,CAAS,EACrF,EAAiB,EAAU,gBAAkB,IAAIC,EAAAA,eAAe,CAAY,EAGlF,MAAO,CAAE,QAAO,WAAU,YAAW,YAAW,QAAO,eAAc,cAAa,iBAAgB,SAFjF,EAAU,UAAY,IAAIC,EAAAA,iBAAiB,EAAU,EAAO,EAAc,CAAc,CAEE,CAC7G"}
1
+ {"version":3,"file":"index.cjs","names":["FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","NodeTreeManifestAdapter","GitDiffService","EditorConfigService","EditTracker","NodeGitStatusAdapter","EditorLauncher","FileEditWorkflow"],"sources":["../../src/container/index.ts"],"sourcesContent":["import { EditorConfigService } from '../adapters/EditorConfigService/EditorConfigService';\nimport { EditorLauncher } from '../adapters/EditorLauncher/EditorLauncher';\nimport { EditTracker } from '../adapters/EditTracker/EditTracker';\nimport { FileEditPaths } from '../adapters/FileEditPaths/FileEditPaths';\nimport { NodeGitStatusAdapter } from '../adapters/node/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../adapters/node/snapshotStore';\nimport { NodeTreeManifestAdapter } from '../adapters/node/treeManifest';\nimport { FileEditWorkflow } from '../tui/fileEditWorkflow';\nimport { GitDiffService } from '../adapters/GitDiffService/GitDiffService';\nimport { TimelineStore } from '../adapters/TimelineStore/TimelineStore';\nimport type { FileEditDependencies } from '../types';\n\n/**\n * Compose the file-edit runtime.\n *\n * Construction order is the dependency order, so the graph is readable top to\n * bottom and a cycle is a compile error rather than a resolution failure at\n * runtime. Pass overrides to substitute a double in tests.\n */\nexport function createFileEditContainer(overrides: Partial<FileEditDependencies> = {}): FileEditDependencies {\n const paths = overrides.paths ?? new FileEditPaths();\n const timeline = overrides.timeline ?? new TimelineStore();\n const snapshots = overrides.snapshots ?? new NodeSnapshotStoreAdapter();\n const manifests = overrides.manifests ?? new NodeTreeManifestAdapter();\n const diffs = overrides.diffs ?? new GitDiffService();\n const editorConfig = overrides.editorConfig ?? new EditorConfigService();\n const editTracker =\n overrides.editTracker ?? new EditTracker(timeline, snapshots, manifests, { git: new NodeGitStatusAdapter() });\n const editorLauncher = overrides.editorLauncher ?? new EditorLauncher(editorConfig);\n const workflow = overrides.workflow ?? new FileEditWorkflow(timeline, diffs, editorConfig, editorLauncher);\n\n return { paths, timeline, snapshots, manifests, diffs, editorConfig, editTracker, editorLauncher, workflow };\n}\n"],"mappings":"4hBAmBA,SAAgB,EAAwB,EAA2C,CAAC,EAAyB,CAC3G,IAAM,EAAQ,EAAU,OAAS,IAAIA,EAAAA,cAC/B,EAAW,EAAU,UAAY,IAAIC,EAAAA,cACrC,EAAY,EAAU,WAAa,IAAIC,EAAAA,yBACvC,EAAY,EAAU,WAAa,IAAIC,EAAAA,wBACvC,EAAQ,EAAU,OAAS,IAAIC,EAAAA,eAC/B,EAAe,EAAU,cAAgB,IAAIC,EAAAA,oBAC7C,EACJ,EAAU,aAAe,IAAIC,EAAAA,YAAY,EAAU,EAAW,EAAW,CAAE,IAAK,IAAIC,EAAAA,oBAAuB,CAAC,EACxG,EAAiB,EAAU,gBAAkB,IAAIC,EAAAA,eAAe,CAAY,EAGlF,MAAO,CAAE,QAAO,WAAU,YAAW,YAAW,QAAO,eAAc,cAAa,iBAAgB,SAFjF,EAAU,UAAY,IAAIC,EAAAA,iBAAiB,EAAU,EAAO,EAAc,CAAc,CAEE,CAC7G"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/container/index.ts"],"mappings":";;;;;;;;;iBAkBgB,wBAAwB,YAAW,QAAQ,wBAA6B"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/container/index.ts"],"mappings":";;;;;;;;;iBAmBgB,wBAAwB,YAAW,QAAQ,wBAA6B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/container/index.ts"],"mappings":";;;;;;;;;iBAkBgB,wBAAwB,YAAW,QAAQ,wBAA6B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/container/index.ts"],"mappings":";;;;;;;;;iBAmBgB,wBAAwB,YAAW,QAAQ,wBAA6B"}
@@ -1,2 +1,2 @@
1
- import{EditorConfigService as e}from"../adapters/EditorConfigService/EditorConfigService.mjs";import{EditorLauncher as t}from"../adapters/EditorLauncher/EditorLauncher.mjs";import{EditTracker as n}from"../adapters/EditTracker/EditTracker.mjs";import{FileEditPaths as r}from"../adapters/FileEditPaths/FileEditPaths.mjs";import{NodeSnapshotStoreAdapter as i}from"../adapters/node/snapshotStore.mjs";import{NodeTreeManifestAdapter as a}from"../adapters/node/treeManifest.mjs";import{FileEditWorkflow as o}from"../tui/fileEditWorkflow.mjs";import{GitDiffService as s}from"../adapters/GitDiffService/GitDiffService.mjs";import{TimelineStore as c}from"../adapters/TimelineStore/TimelineStore.mjs";function l(l={}){let u=l.paths??new r,d=l.timeline??new c,f=l.snapshots??new i,p=l.manifests??new a,m=l.diffs??new s,h=l.editorConfig??new e,g=l.editTracker??new n(d,f,p),_=l.editorLauncher??new t(h);return{paths:u,timeline:d,snapshots:f,manifests:p,diffs:m,editorConfig:h,editTracker:g,editorLauncher:_,workflow:l.workflow??new o(d,m,h,_)}}export{l as createFileEditContainer};
1
+ import{EditorConfigService as e}from"../adapters/EditorConfigService/EditorConfigService.mjs";import{EditorLauncher as t}from"../adapters/EditorLauncher/EditorLauncher.mjs";import{EditTracker as n}from"../adapters/EditTracker/EditTracker.mjs";import{FileEditPaths as r}from"../adapters/FileEditPaths/FileEditPaths.mjs";import{NodeGitStatusAdapter as i}from"../adapters/node/gitStatus.mjs";import{NodeSnapshotStoreAdapter as a}from"../adapters/node/snapshotStore.mjs";import{NodeTreeManifestAdapter as o}from"../adapters/node/treeManifest.mjs";import{FileEditWorkflow as s}from"../tui/fileEditWorkflow.mjs";import{GitDiffService as c}from"../adapters/GitDiffService/GitDiffService.mjs";import{TimelineStore as l}from"../adapters/TimelineStore/TimelineStore.mjs";function u(u={}){let d=u.paths??new r,f=u.timeline??new l,p=u.snapshots??new a,m=u.manifests??new o,h=u.diffs??new c,g=u.editorConfig??new e,_=u.editTracker??new n(f,p,m,{git:new i}),v=u.editorLauncher??new t(g);return{paths:d,timeline:f,snapshots:p,manifests:m,diffs:h,editorConfig:g,editTracker:_,editorLauncher:v,workflow:u.workflow??new s(f,h,g,v)}}export{u as createFileEditContainer};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/container/index.ts"],"sourcesContent":["import { EditorConfigService } from '../adapters/EditorConfigService/EditorConfigService';\nimport { EditorLauncher } from '../adapters/EditorLauncher/EditorLauncher';\nimport { EditTracker } from '../adapters/EditTracker/EditTracker';\nimport { FileEditPaths } from '../adapters/FileEditPaths/FileEditPaths';\nimport { NodeSnapshotStoreAdapter } from '../adapters/node/snapshotStore';\nimport { NodeTreeManifestAdapter } from '../adapters/node/treeManifest';\nimport { FileEditWorkflow } from '../tui/fileEditWorkflow';\nimport { GitDiffService } from '../adapters/GitDiffService/GitDiffService';\nimport { TimelineStore } from '../adapters/TimelineStore/TimelineStore';\nimport type { FileEditDependencies } from '../types';\n\n/**\n * Compose the file-edit runtime.\n *\n * Construction order is the dependency order, so the graph is readable top to\n * bottom and a cycle is a compile error rather than a resolution failure at\n * runtime. Pass overrides to substitute a double in tests.\n */\nexport function createFileEditContainer(overrides: Partial<FileEditDependencies> = {}): FileEditDependencies {\n const paths = overrides.paths ?? new FileEditPaths();\n const timeline = overrides.timeline ?? new TimelineStore();\n const snapshots = overrides.snapshots ?? new NodeSnapshotStoreAdapter();\n const manifests = overrides.manifests ?? new NodeTreeManifestAdapter();\n const diffs = overrides.diffs ?? new GitDiffService();\n const editorConfig = overrides.editorConfig ?? new EditorConfigService();\n const editTracker = overrides.editTracker ?? new EditTracker(timeline, snapshots, manifests);\n const editorLauncher = overrides.editorLauncher ?? new EditorLauncher(editorConfig);\n const workflow = overrides.workflow ?? new FileEditWorkflow(timeline, diffs, editorConfig, editorLauncher);\n\n return { paths, timeline, snapshots, manifests, diffs, editorConfig, editTracker, editorLauncher, workflow };\n}\n"],"mappings":"mrBAkBA,SAAgB,EAAwB,EAA2C,CAAC,EAAyB,CAC3G,IAAM,EAAQ,EAAU,OAAS,IAAI,EAC/B,EAAW,EAAU,UAAY,IAAI,EACrC,EAAY,EAAU,WAAa,IAAI,EACvC,EAAY,EAAU,WAAa,IAAI,EACvC,EAAQ,EAAU,OAAS,IAAI,EAC/B,EAAe,EAAU,cAAgB,IAAI,EAC7C,EAAc,EAAU,aAAe,IAAI,EAAY,EAAU,EAAW,CAAS,EACrF,EAAiB,EAAU,gBAAkB,IAAI,EAAe,CAAY,EAGlF,MAAO,CAAE,QAAO,WAAU,YAAW,YAAW,QAAO,eAAc,cAAa,iBAAgB,SAFjF,EAAU,UAAY,IAAI,EAAiB,EAAU,EAAO,EAAc,CAAc,CAEE,CAC7G"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/container/index.ts"],"sourcesContent":["import { EditorConfigService } from '../adapters/EditorConfigService/EditorConfigService';\nimport { EditorLauncher } from '../adapters/EditorLauncher/EditorLauncher';\nimport { EditTracker } from '../adapters/EditTracker/EditTracker';\nimport { FileEditPaths } from '../adapters/FileEditPaths/FileEditPaths';\nimport { NodeGitStatusAdapter } from '../adapters/node/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../adapters/node/snapshotStore';\nimport { NodeTreeManifestAdapter } from '../adapters/node/treeManifest';\nimport { FileEditWorkflow } from '../tui/fileEditWorkflow';\nimport { GitDiffService } from '../adapters/GitDiffService/GitDiffService';\nimport { TimelineStore } from '../adapters/TimelineStore/TimelineStore';\nimport type { FileEditDependencies } from '../types';\n\n/**\n * Compose the file-edit runtime.\n *\n * Construction order is the dependency order, so the graph is readable top to\n * bottom and a cycle is a compile error rather than a resolution failure at\n * runtime. Pass overrides to substitute a double in tests.\n */\nexport function createFileEditContainer(overrides: Partial<FileEditDependencies> = {}): FileEditDependencies {\n const paths = overrides.paths ?? new FileEditPaths();\n const timeline = overrides.timeline ?? new TimelineStore();\n const snapshots = overrides.snapshots ?? new NodeSnapshotStoreAdapter();\n const manifests = overrides.manifests ?? new NodeTreeManifestAdapter();\n const diffs = overrides.diffs ?? new GitDiffService();\n const editorConfig = overrides.editorConfig ?? new EditorConfigService();\n const editTracker =\n overrides.editTracker ?? new EditTracker(timeline, snapshots, manifests, { git: new NodeGitStatusAdapter() });\n const editorLauncher = overrides.editorLauncher ?? new EditorLauncher(editorConfig);\n const workflow = overrides.workflow ?? new FileEditWorkflow(timeline, diffs, editorConfig, editorLauncher);\n\n return { paths, timeline, snapshots, manifests, diffs, editorConfig, editTracker, editorLauncher, workflow };\n}\n"],"mappings":"yvBAmBA,SAAgB,EAAwB,EAA2C,CAAC,EAAyB,CAC3G,IAAM,EAAQ,EAAU,OAAS,IAAI,EAC/B,EAAW,EAAU,UAAY,IAAI,EACrC,EAAY,EAAU,WAAa,IAAI,EACvC,EAAY,EAAU,WAAa,IAAI,EACvC,EAAQ,EAAU,OAAS,IAAI,EAC/B,EAAe,EAAU,cAAgB,IAAI,EAC7C,EACJ,EAAU,aAAe,IAAI,EAAY,EAAU,EAAW,EAAW,CAAE,IAAK,IAAI,CAAuB,CAAC,EACxG,EAAiB,EAAU,gBAAkB,IAAI,EAAe,CAAY,EAGlF,MAAO,CAAE,QAAO,WAAU,YAAW,YAAW,QAAO,eAAc,cAAa,iBAAgB,SAFjF,EAAU,UAAY,IAAI,EAAiB,EAAU,EAAO,EAAc,CAAc,CAEE,CAC7G"}
@@ -1,3 +1,3 @@
1
- const e=new Set([`edit`,`write`,`bash`,`user`]);function t(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function n(e){return typeof e==`string`&&e!==``?e:void 0}function r(e){return typeof e==`number`&&Number.isFinite(e)&&e>=0?e:void 0}function i(i){if(!t(i))return null;let{version:a,path:o,tool:s,at:c}=i;return typeof o!=`string`||o===``||typeof c!=`number`||!Number.isFinite(c)||typeof s!=`string`||!e.has(s)?null:a===1?s===`user`?null:{version:1,path:o,tool:s,at:c}:a===2?{version:2,path:o,tool:s,at:c,origin:i.origin===`scan`?`scan`:`tool`,...n(i.before)===void 0?{}:{before:i.before},...n(i.after)===void 0?{}:{after:i.after},...r(i.additions)===void 0?{}:{additions:i.additions},...r(i.removals)===void 0?{}:{removals:i.removals}}:null}function a(e,t){let n=[];for(let r of e.split(`
2
- `)){if(r===``)continue;let e;try{e=JSON.parse(r)}catch{t?.(r);continue}let a=i(e);a===null?t?.(r):n.push(a)}return n}function o(e){let t=new Map;for(let n of e){let e=t.get(n.path);t.set(n.path,{path:n.path,tool:n.at>=(e?.at??0)?n.tool:e?.tool??n.tool,at:Math.max(n.at,e?.at??0),count:(e?.count??0)+1})}return[...t.values()].sort((e,t)=>t.at-e.at)}function s(e,t){return e.filter(e=>e.path===t).sort((e,t)=>e.at-t.at).map((e,t)=>{let n={index:t+1,tool:e.tool,at:e.at,origin:e.version===2?e.origin:`scan`};return e.version===2?{...n,...e.before===void 0?{}:{before:e.before},...e.after===void 0?{}:{after:e.after},...e.additions===void 0?{}:{additions:e.additions},...e.removals===void 0?{}:{removals:e.removals}}:n})}function c(e){return e.some(e=>e.before!==void 0)}function l(e){return e.find(e=>e.before!==void 0)?.before}exports.baselineOf=l,exports.foldEntries=o,exports.foldVersions=s,exports.isDiffable=c,exports.parseTimeline=a,exports.parseTimelineEvent=i;
1
+ const e=new Set([`edit`,`write`,`bash`,`user`]);function t(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function n(e){return typeof e==`string`&&e!==``?e:void 0}function r(e){return typeof e==`number`&&Number.isFinite(e)&&e>=0?e:void 0}function i(i){if(!t(i))return null;let{version:a,path:o,tool:s,at:c}=i;return typeof o!=`string`||o===``||typeof c!=`number`||!Number.isFinite(c)||typeof s!=`string`||!e.has(s)?null:a===1?s===`user`?null:{version:1,path:o,tool:s,at:c}:a===2?{version:2,path:o,tool:s,at:c,origin:i.origin===`scan`?`scan`:`tool`,...n(i.before)===void 0?{}:{before:i.before},...n(i.after)===void 0?{}:{after:i.after},...r(i.additions)===void 0?{}:{additions:i.additions},...r(i.removals)===void 0?{}:{removals:i.removals},...i.verified===!0?{verified:!0}:{}}:null}function a(e,t){let n=[];for(let r of e.split(`
2
+ `)){if(r===``)continue;let e;try{e=JSON.parse(r)}catch{t?.(r);continue}let a=i(e);a===null?t?.(r):n.push(a)}return n}function o(e){return e.version===2?e.origin!==`scan`||e.verified===!0:e.tool!==`bash`}function s(e){return e.filter(o)}function c(e){let t=new Map;for(let n of e){let e=t.get(n.path);t.set(n.path,{path:n.path,tool:n.at>=(e?.at??0)?n.tool:e?.tool??n.tool,at:Math.max(n.at,e?.at??0),count:(e?.count??0)+1})}return[...t.values()].sort((e,t)=>t.at-e.at)}function l(e,t){return e.filter(e=>e.path===t).sort((e,t)=>e.at-t.at).map((e,t)=>{let n={index:t+1,tool:e.tool,at:e.at,origin:e.version===2?e.origin:`scan`};return e.version===2?{...n,...e.before===void 0?{}:{before:e.before},...e.after===void 0?{}:{after:e.after},...e.additions===void 0?{}:{additions:e.additions},...e.removals===void 0?{}:{removals:e.removals},...e.verified===!0?{verified:!0}:{}}:n})}function u(e){return e.some(e=>e.before!==void 0)}function d(e){return e.find(e=>e.before!==void 0)?.before}exports.baselineOf=d,exports.confirmedChanges=s,exports.foldEntries=c,exports.foldVersions=l,exports.isConfirmedChange=o,exports.isDiffable=u,exports.parseTimeline=a,exports.parseTimelineEvent=i;
3
3
  //# sourceMappingURL=fileChanges.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileChanges.cjs","names":[],"sources":["../../src/services/fileChanges.ts"],"sourcesContent":["import type { FileEditEntry, FileEditVersion, LegacyTimelineEvent, TimelineEvent } from '../types/domain.ts';\n\n/**\n * Fold a session's recorded changes into the per-file rows and version\n * histories the surfaces show.\n *\n * The timeline is append-only, one line per change, so every reader folds it\n * the same way and the folding belongs somewhere both the store and its tests\n * can reach without a filesystem. Version 1 lines are still read: a session\n * already running when the package updates keeps appending to the file it\n * opened, and dropping its earlier lines would blank a list mid-session.\n */\n\n/** Either shape the timeline file can hold. */\nexport type AnyTimelineEvent = TimelineEvent | LegacyTimelineEvent;\n\nconst TOOLS = new Set(['edit', 'write', 'bash', 'user']);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction optionalCount(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** Narrows one parsed line, or answers null so a malformed line can be skipped. */\nexport function parseTimelineEvent(value: unknown): AnyTimelineEvent | null {\n if (!isRecord(value)) return null;\n const { version, path: filePath, tool, at } = value;\n if (typeof filePath !== 'string' || filePath === '') return null;\n if (typeof at !== 'number' || !Number.isFinite(at)) return null;\n if (typeof tool !== 'string' || !TOOLS.has(tool)) return null;\n\n if (version === 1) {\n if (tool === 'user') return null; // The version 1 vocabulary had no manual saves.\n return { version: 1, path: filePath, tool: tool as LegacyTimelineEvent['tool'], at };\n }\n if (version !== 2) return null;\n const origin = value.origin === 'scan' ? 'scan' : 'tool';\n return {\n version: 2,\n path: filePath,\n tool: tool as TimelineEvent['tool'],\n at,\n origin,\n ...(optionalString(value.before) === undefined ? {} : { before: value.before as string }),\n ...(optionalString(value.after) === undefined ? {} : { after: value.after as string }),\n ...(optionalCount(value.additions) === undefined ? {} : { additions: value.additions as number }),\n ...(optionalCount(value.removals) === undefined ? {} : { removals: value.removals as number }),\n };\n}\n\n/** Every recorded line of the timeline, malformed ones dropped, in file order. */\nexport function parseTimeline(content: string, onMalformed?: (line: string) => void): AnyTimelineEvent[] {\n const events: AnyTimelineEvent[] = [];\n for (const line of content.split('\\n')) {\n if (line === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n onMalformed?.(line);\n continue;\n }\n const event = parseTimelineEvent(parsed);\n if (event === null) onMalformed?.(line);\n else events.push(event);\n }\n return events;\n}\n\n/** One row per file, newest change first, which is the order both docks list in. */\nexport function foldEntries(events: readonly AnyTimelineEvent[]): FileEditEntry[] {\n const folded = new Map<string, FileEditEntry>();\n for (const event of events) {\n const current = folded.get(event.path);\n folded.set(event.path, {\n path: event.path,\n tool: event.at >= (current?.at ?? 0) ? event.tool : (current?.tool ?? event.tool),\n at: Math.max(event.at, current?.at ?? 0),\n count: (current?.count ?? 0) + 1,\n });\n }\n return [...folded.values()].sort((left, right) => right.at - left.at);\n}\n\n/** One file's history, oldest first, numbered so a reader can name a version. */\nexport function foldVersions(events: readonly AnyTimelineEvent[], filePath: string): FileEditVersion[] {\n return events\n .filter((event) => event.path === filePath)\n .sort((left, right) => left.at - right.at)\n .map((event, offset) => {\n const version: FileEditVersion = {\n index: offset + 1,\n tool: event.tool,\n at: event.at,\n origin: event.version === 2 ? event.origin : 'scan',\n };\n if (event.version !== 2) return version;\n return {\n ...version,\n ...(event.before === undefined ? {} : { before: event.before }),\n ...(event.after === undefined ? {} : { after: event.after }),\n ...(event.additions === undefined ? {} : { additions: event.additions }),\n ...(event.removals === undefined ? {} : { removals: event.removals }),\n };\n });\n}\n\n/**\n * Whether a file can be diffed at all: some version has to have captured what\n * the file held before it changed. A file only ever seen after the fact, which\n * is every file a bash script wrote, never can be.\n */\nexport function isDiffable(versions: readonly FileEditVersion[]): boolean {\n return versions.some((version) => version.before !== undefined);\n}\n\n/** The oldest captured baseline for a file, which is what the whole-session diff starts from. */\nexport function baselineOf(versions: readonly FileEditVersion[]): string | undefined {\n return versions.find((version) => version.before !== undefined)?.before;\n}\n"],"mappings":"AAgBA,MAAM,EAAQ,IAAI,IAAI,CAAC,OAAQ,QAAS,OAAQ,MAAM,CAAC,EAEvD,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,CAC5E,CAEA,SAAS,EAAe,EAAoC,CAC1D,OAAO,OAAO,GAAU,UAAY,IAAU,GAAK,EAAQ,IAAA,EAC7D,CAEA,SAAS,EAAc,EAAoC,CACzD,OAAO,OAAO,GAAU,UAAY,OAAO,SAAS,CAAK,GAAK,GAAS,EAAI,EAAQ,IAAA,EACrF,CAGA,SAAgB,EAAmB,EAAyC,CAC1E,GAAI,CAAC,EAAS,CAAK,EAAG,OAAO,KAC7B,GAAM,CAAE,UAAS,KAAM,EAAU,OAAM,MAAO,EAW9C,OAVI,OAAO,GAAa,UAAY,IAAa,IAC7C,OAAO,GAAO,UAAY,CAAC,OAAO,SAAS,CAAE,GAC7C,OAAO,GAAS,UAAY,CAAC,EAAM,IAAI,CAAI,EAAU,KAErD,IAAY,EACV,IAAS,OAAe,KACrB,CAAE,QAAS,EAAG,KAAM,EAAgB,OAAqC,IAAG,EAEjF,IAAY,EAET,CACL,QAAS,EACT,KAAM,EACA,OACN,KACA,OANa,EAAM,SAAW,OAAS,OAAS,OAOhD,GAAI,EAAe,EAAM,MAAM,IAAM,IAAA,GAAY,CAAC,EAAI,CAAE,OAAQ,EAAM,MAAiB,EACvF,GAAI,EAAe,EAAM,KAAK,IAAM,IAAA,GAAY,CAAC,EAAI,CAAE,MAAO,EAAM,KAAgB,EACpF,GAAI,EAAc,EAAM,SAAS,IAAM,IAAA,GAAY,CAAC,EAAI,CAAE,UAAW,EAAM,SAAoB,EAC/F,GAAI,EAAc,EAAM,QAAQ,IAAM,IAAA,GAAY,CAAC,EAAI,CAAE,SAAU,EAAM,QAAmB,CAC9F,EAZ0B,IAa5B,CAGA,SAAgB,EAAc,EAAiB,EAA0D,CACvG,IAAM,EAA6B,CAAC,EACpC,IAAK,IAAM,KAAQ,EAAQ,MAAM;CAAI,EAAG,CACtC,GAAI,IAAS,GAAI,SACjB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,CAAI,CAC1B,MAAQ,CACN,IAAc,CAAI,EAClB,QACF,CACA,IAAM,EAAQ,EAAmB,CAAM,EACnC,IAAU,KAAM,IAAc,CAAI,EACjC,EAAO,KAAK,CAAK,CACxB,CACA,OAAO,CACT,CAGA,SAAgB,EAAY,EAAsD,CAChF,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAS,EAAQ,CAC1B,IAAM,EAAU,EAAO,IAAI,EAAM,IAAI,EACrC,EAAO,IAAI,EAAM,KAAM,CACrB,KAAM,EAAM,KACZ,KAAM,EAAM,KAAO,GAAS,IAAM,GAAK,EAAM,KAAQ,GAAS,MAAQ,EAAM,KAC5E,GAAI,KAAK,IAAI,EAAM,GAAI,GAAS,IAAM,CAAC,EACvC,OAAQ,GAAS,OAAS,GAAK,CACjC,CAAC,CACH,CACA,MAAO,CAAC,GAAG,EAAO,OAAO,CAAC,CAAC,CAAC,MAAM,EAAM,IAAU,EAAM,GAAK,EAAK,EAAE,CACtE,CAGA,SAAgB,EAAa,EAAqC,EAAqC,CACrG,OAAO,EACJ,OAAQ,GAAU,EAAM,OAAS,CAAQ,CAAC,CAC1C,MAAM,EAAM,IAAU,EAAK,GAAK,EAAM,EAAE,CAAC,CACzC,KAAK,EAAO,IAAW,CACtB,IAAM,EAA2B,CAC/B,MAAO,EAAS,EAChB,KAAM,EAAM,KACZ,GAAI,EAAM,GACV,OAAQ,EAAM,UAAY,EAAI,EAAM,OAAS,MAC/C,EAEA,OADI,EAAM,UAAY,EACf,CACL,GAAG,EACH,GAAI,EAAM,SAAW,IAAA,GAAY,CAAC,EAAI,CAAE,OAAQ,EAAM,MAAO,EAC7D,GAAI,EAAM,QAAU,IAAA,GAAY,CAAC,EAAI,CAAE,MAAO,EAAM,KAAM,EAC1D,GAAI,EAAM,YAAc,IAAA,GAAY,CAAC,EAAI,CAAE,UAAW,EAAM,SAAU,EACtE,GAAI,EAAM,WAAa,IAAA,GAAY,CAAC,EAAI,CAAE,SAAU,EAAM,QAAS,CACrE,EAPgC,CAQlC,CAAC,CACL,CAOA,SAAgB,EAAW,EAA+C,CACxE,OAAO,EAAS,KAAM,GAAY,EAAQ,SAAW,IAAA,EAAS,CAChE,CAGA,SAAgB,EAAW,EAA0D,CACnF,OAAO,EAAS,KAAM,GAAY,EAAQ,SAAW,IAAA,EAAS,CAAC,EAAE,MACnE"}
1
+ {"version":3,"file":"fileChanges.cjs","names":[],"sources":["../../src/services/fileChanges.ts"],"sourcesContent":["import type { FileEditEntry, FileEditVersion, LegacyTimelineEvent, TimelineEvent } from '../types/domain.ts';\n\n/**\n * Fold a session's recorded changes into the per-file rows and version\n * histories the surfaces show.\n *\n * The timeline is append-only, one line per change, so every reader folds it\n * the same way and the folding belongs somewhere both the store and its tests\n * can reach without a filesystem. Version 1 lines are still read: a session\n * already running when the package updates keeps appending to the file it\n * opened, and dropping its earlier lines would blank a list mid-session.\n */\n\n/** Either shape the timeline file can hold. */\nexport type AnyTimelineEvent = TimelineEvent | LegacyTimelineEvent;\n\nconst TOOLS = new Set(['edit', 'write', 'bash', 'user']);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction optionalCount(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** Narrows one parsed line, or answers null so a malformed line can be skipped. */\nexport function parseTimelineEvent(value: unknown): AnyTimelineEvent | null {\n if (!isRecord(value)) return null;\n const { version, path: filePath, tool, at } = value;\n if (typeof filePath !== 'string' || filePath === '') return null;\n if (typeof at !== 'number' || !Number.isFinite(at)) return null;\n if (typeof tool !== 'string' || !TOOLS.has(tool)) return null;\n\n if (version === 1) {\n if (tool === 'user') return null; // The version 1 vocabulary had no manual saves.\n return { version: 1, path: filePath, tool: tool as LegacyTimelineEvent['tool'], at };\n }\n if (version !== 2) return null;\n const origin = value.origin === 'scan' ? 'scan' : 'tool';\n return {\n version: 2,\n path: filePath,\n tool: tool as TimelineEvent['tool'],\n at,\n origin,\n ...(optionalString(value.before) === undefined ? {} : { before: value.before as string }),\n ...(optionalString(value.after) === undefined ? {} : { after: value.after as string }),\n ...(optionalCount(value.additions) === undefined ? {} : { additions: value.additions as number }),\n ...(optionalCount(value.removals) === undefined ? {} : { removals: value.removals as number }),\n ...(value.verified === true ? { verified: true } : {}),\n };\n}\n\n/** Every recorded line of the timeline, malformed ones dropped, in file order. */\nexport function parseTimeline(content: string, onMalformed?: (line: string) => void): AnyTimelineEvent[] {\n const events: AnyTimelineEvent[] = [];\n for (const line of content.split('\\n')) {\n if (line === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n onMalformed?.(line);\n continue;\n }\n const event = parseTimelineEvent(parsed);\n if (event === null) onMalformed?.(line);\n else events.push(event);\n }\n return events;\n}\n\n/**\n * Whether a recorded change is evidence that the file's content actually moved.\n *\n * A tool change names its file and reads both sides, so it always is. A scan\n * change is a tree comparison of size and modification time, which a checkout,\n * an install, or a command that merely touched a file moves as readily as an\n * edit does; only a scan the tracker confirmed carries `verified`. Version 1\n * lines predate the distinction entirely, so a bash one cannot be trusted.\n */\nexport function isConfirmedChange(event: AnyTimelineEvent): boolean {\n if (event.version !== 2) return event.tool !== 'bash';\n return event.origin !== 'scan' || event.verified === true;\n}\n\n/** The recorded changes worth listing, which is every change but an unconfirmed touch. */\nexport function confirmedChanges(events: readonly AnyTimelineEvent[]): AnyTimelineEvent[] {\n return events.filter(isConfirmedChange);\n}\n/** One row per file, newest change first, which is the order both docks list in. */\nexport function foldEntries(events: readonly AnyTimelineEvent[]): FileEditEntry[] {\n const folded = new Map<string, FileEditEntry>();\n for (const event of events) {\n const current = folded.get(event.path);\n folded.set(event.path, {\n path: event.path,\n tool: event.at >= (current?.at ?? 0) ? event.tool : (current?.tool ?? event.tool),\n at: Math.max(event.at, current?.at ?? 0),\n count: (current?.count ?? 0) + 1,\n });\n }\n return [...folded.values()].sort((left, right) => right.at - left.at);\n}\n\n/** One file's history, oldest first, numbered so a reader can name a version. */\nexport function foldVersions(events: readonly AnyTimelineEvent[], filePath: string): FileEditVersion[] {\n return events\n .filter((event) => event.path === filePath)\n .sort((left, right) => left.at - right.at)\n .map((event, offset) => {\n const version: FileEditVersion = {\n index: offset + 1,\n tool: event.tool,\n at: event.at,\n origin: event.version === 2 ? event.origin : 'scan',\n };\n if (event.version !== 2) return version;\n return {\n ...version,\n ...(event.before === undefined ? {} : { before: event.before }),\n ...(event.after === undefined ? {} : { after: event.after }),\n ...(event.additions === undefined ? {} : { additions: event.additions }),\n ...(event.removals === undefined ? {} : { removals: event.removals }),\n ...(event.verified === true ? { verified: true } : {}),\n };\n });\n}\n\n/**\n * Whether a file can be diffed at all: some version has to have captured what\n * the file held before it changed. A file only ever seen after the fact, which\n * is every file a bash script wrote, never can be.\n */\nexport function isDiffable(versions: readonly FileEditVersion[]): boolean {\n return versions.some((version) => version.before !== undefined);\n}\n\n/** The oldest captured baseline for a file, which is what the whole-session diff starts from. */\nexport function baselineOf(versions: readonly FileEditVersion[]): string | undefined {\n return versions.find((version) => version.before !== undefined)?.before;\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- const e=new Set([`edit`,`write`,`bash`,`user`]);function t(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function n(e){return typeof e==`string`&&e!==``?e:void 0}function r(e){return typeof e==`number`&&Number.isFinite(e)&&e>=0?e:void 0}function i(i){if(!t(i))return null;let{version:a,path:o,tool:s,at:c}=i;return typeof o!=`string`||o===``||typeof c!=`number`||!Number.isFinite(c)||typeof s!=`string`||!e.has(s)?null:a===1?s===`user`?null:{version:1,path:o,tool:s,at:c}:a===2?{version:2,path:o,tool:s,at:c,origin:i.origin===`scan`?`scan`:`tool`,...n(i.before)===void 0?{}:{before:i.before},...n(i.after)===void 0?{}:{after:i.after},...r(i.additions)===void 0?{}:{additions:i.additions},...r(i.removals)===void 0?{}:{removals:i.removals}}:null}function a(e,t){let n=[];for(let r of e.split(`
2
- `)){if(r===``)continue;let e;try{e=JSON.parse(r)}catch{t?.(r);continue}let a=i(e);a===null?t?.(r):n.push(a)}return n}function o(e){let t=new Map;for(let n of e){let e=t.get(n.path);t.set(n.path,{path:n.path,tool:n.at>=(e?.at??0)?n.tool:e?.tool??n.tool,at:Math.max(n.at,e?.at??0),count:(e?.count??0)+1})}return[...t.values()].sort((e,t)=>t.at-e.at)}function s(e,t){return e.filter(e=>e.path===t).sort((e,t)=>e.at-t.at).map((e,t)=>{let n={index:t+1,tool:e.tool,at:e.at,origin:e.version===2?e.origin:`scan`};return e.version===2?{...n,...e.before===void 0?{}:{before:e.before},...e.after===void 0?{}:{after:e.after},...e.additions===void 0?{}:{additions:e.additions},...e.removals===void 0?{}:{removals:e.removals}}:n})}function c(e){return e.some(e=>e.before!==void 0)}function l(e){return e.find(e=>e.before!==void 0)?.before}export{l as baselineOf,o as foldEntries,s as foldVersions,c as isDiffable,a as parseTimeline,i as parseTimelineEvent};
1
+ const e=new Set([`edit`,`write`,`bash`,`user`]);function t(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function n(e){return typeof e==`string`&&e!==``?e:void 0}function r(e){return typeof e==`number`&&Number.isFinite(e)&&e>=0?e:void 0}function i(i){if(!t(i))return null;let{version:a,path:o,tool:s,at:c}=i;return typeof o!=`string`||o===``||typeof c!=`number`||!Number.isFinite(c)||typeof s!=`string`||!e.has(s)?null:a===1?s===`user`?null:{version:1,path:o,tool:s,at:c}:a===2?{version:2,path:o,tool:s,at:c,origin:i.origin===`scan`?`scan`:`tool`,...n(i.before)===void 0?{}:{before:i.before},...n(i.after)===void 0?{}:{after:i.after},...r(i.additions)===void 0?{}:{additions:i.additions},...r(i.removals)===void 0?{}:{removals:i.removals},...i.verified===!0?{verified:!0}:{}}:null}function a(e,t){let n=[];for(let r of e.split(`
2
+ `)){if(r===``)continue;let e;try{e=JSON.parse(r)}catch{t?.(r);continue}let a=i(e);a===null?t?.(r):n.push(a)}return n}function o(e){return e.version===2?e.origin!==`scan`||e.verified===!0:e.tool!==`bash`}function s(e){return e.filter(o)}function c(e){let t=new Map;for(let n of e){let e=t.get(n.path);t.set(n.path,{path:n.path,tool:n.at>=(e?.at??0)?n.tool:e?.tool??n.tool,at:Math.max(n.at,e?.at??0),count:(e?.count??0)+1})}return[...t.values()].sort((e,t)=>t.at-e.at)}function l(e,t){return e.filter(e=>e.path===t).sort((e,t)=>e.at-t.at).map((e,t)=>{let n={index:t+1,tool:e.tool,at:e.at,origin:e.version===2?e.origin:`scan`};return e.version===2?{...n,...e.before===void 0?{}:{before:e.before},...e.after===void 0?{}:{after:e.after},...e.additions===void 0?{}:{additions:e.additions},...e.removals===void 0?{}:{removals:e.removals},...e.verified===!0?{verified:!0}:{}}:n})}function u(e){return e.some(e=>e.before!==void 0)}function d(e){return e.find(e=>e.before!==void 0)?.before}export{d as baselineOf,s as confirmedChanges,c as foldEntries,l as foldVersions,o as isConfirmedChange,u as isDiffable,a as parseTimeline,i as parseTimelineEvent};
3
3
  //# sourceMappingURL=fileChanges.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileChanges.mjs","names":[],"sources":["../../src/services/fileChanges.ts"],"sourcesContent":["import type { FileEditEntry, FileEditVersion, LegacyTimelineEvent, TimelineEvent } from '../types/domain.ts';\n\n/**\n * Fold a session's recorded changes into the per-file rows and version\n * histories the surfaces show.\n *\n * The timeline is append-only, one line per change, so every reader folds it\n * the same way and the folding belongs somewhere both the store and its tests\n * can reach without a filesystem. Version 1 lines are still read: a session\n * already running when the package updates keeps appending to the file it\n * opened, and dropping its earlier lines would blank a list mid-session.\n */\n\n/** Either shape the timeline file can hold. */\nexport type AnyTimelineEvent = TimelineEvent | LegacyTimelineEvent;\n\nconst TOOLS = new Set(['edit', 'write', 'bash', 'user']);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction optionalCount(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** Narrows one parsed line, or answers null so a malformed line can be skipped. */\nexport function parseTimelineEvent(value: unknown): AnyTimelineEvent | null {\n if (!isRecord(value)) return null;\n const { version, path: filePath, tool, at } = value;\n if (typeof filePath !== 'string' || filePath === '') return null;\n if (typeof at !== 'number' || !Number.isFinite(at)) return null;\n if (typeof tool !== 'string' || !TOOLS.has(tool)) return null;\n\n if (version === 1) {\n if (tool === 'user') return null; // The version 1 vocabulary had no manual saves.\n return { version: 1, path: filePath, tool: tool as LegacyTimelineEvent['tool'], at };\n }\n if (version !== 2) return null;\n const origin = value.origin === 'scan' ? 'scan' : 'tool';\n return {\n version: 2,\n path: filePath,\n tool: tool as TimelineEvent['tool'],\n at,\n origin,\n ...(optionalString(value.before) === undefined ? {} : { before: value.before as string }),\n ...(optionalString(value.after) === undefined ? {} : { after: value.after as string }),\n ...(optionalCount(value.additions) === undefined ? {} : { additions: value.additions as number }),\n ...(optionalCount(value.removals) === undefined ? {} : { removals: value.removals as number }),\n };\n}\n\n/** Every recorded line of the timeline, malformed ones dropped, in file order. */\nexport function parseTimeline(content: string, onMalformed?: (line: string) => void): AnyTimelineEvent[] {\n const events: AnyTimelineEvent[] = [];\n for (const line of content.split('\\n')) {\n if (line === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n onMalformed?.(line);\n continue;\n }\n const event = parseTimelineEvent(parsed);\n if (event === null) onMalformed?.(line);\n else events.push(event);\n }\n return events;\n}\n\n/** One row per file, newest change first, which is the order both docks list in. */\nexport function foldEntries(events: readonly AnyTimelineEvent[]): FileEditEntry[] {\n const folded = new Map<string, FileEditEntry>();\n for (const event of events) {\n const current = folded.get(event.path);\n folded.set(event.path, {\n path: event.path,\n tool: event.at >= (current?.at ?? 0) ? event.tool : (current?.tool ?? event.tool),\n at: Math.max(event.at, current?.at ?? 0),\n count: (current?.count ?? 0) + 1,\n });\n }\n return [...folded.values()].sort((left, right) => right.at - left.at);\n}\n\n/** One file's history, oldest first, numbered so a reader can name a version. */\nexport function foldVersions(events: readonly AnyTimelineEvent[], filePath: string): FileEditVersion[] {\n return events\n .filter((event) => event.path === filePath)\n .sort((left, right) => left.at - right.at)\n .map((event, offset) => {\n const version: FileEditVersion = {\n index: offset + 1,\n tool: event.tool,\n at: event.at,\n origin: event.version === 2 ? event.origin : 'scan',\n };\n if (event.version !== 2) return version;\n return {\n ...version,\n ...(event.before === undefined ? {} : { before: event.before }),\n ...(event.after === undefined ? {} : { after: event.after }),\n ...(event.additions === undefined ? {} : { additions: event.additions }),\n ...(event.removals === undefined ? {} : { removals: event.removals }),\n };\n });\n}\n\n/**\n * Whether a file can be diffed at all: some version has to have captured what\n * the file held before it changed. A file only ever seen after the fact, which\n * is every file a bash script wrote, never can be.\n */\nexport function isDiffable(versions: readonly FileEditVersion[]): boolean {\n return versions.some((version) => version.before !== undefined);\n}\n\n/** The oldest captured baseline for a file, which is what the whole-session diff starts from. */\nexport function baselineOf(versions: readonly FileEditVersion[]): string | undefined {\n return versions.find((version) => version.before !== undefined)?.before;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileChanges.mjs","names":[],"sources":["../../src/services/fileChanges.ts"],"sourcesContent":["import type { FileEditEntry, FileEditVersion, LegacyTimelineEvent, TimelineEvent } from '../types/domain.ts';\n\n/**\n * Fold a session's recorded changes into the per-file rows and version\n * histories the surfaces show.\n *\n * The timeline is append-only, one line per change, so every reader folds it\n * the same way and the folding belongs somewhere both the store and its tests\n * can reach without a filesystem. Version 1 lines are still read: a session\n * already running when the package updates keeps appending to the file it\n * opened, and dropping its earlier lines would blank a list mid-session.\n */\n\n/** Either shape the timeline file can hold. */\nexport type AnyTimelineEvent = TimelineEvent | LegacyTimelineEvent;\n\nconst TOOLS = new Set(['edit', 'write', 'bash', 'user']);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction optionalCount(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** Narrows one parsed line, or answers null so a malformed line can be skipped. */\nexport function parseTimelineEvent(value: unknown): AnyTimelineEvent | null {\n if (!isRecord(value)) return null;\n const { version, path: filePath, tool, at } = value;\n if (typeof filePath !== 'string' || filePath === '') return null;\n if (typeof at !== 'number' || !Number.isFinite(at)) return null;\n if (typeof tool !== 'string' || !TOOLS.has(tool)) return null;\n\n if (version === 1) {\n if (tool === 'user') return null; // The version 1 vocabulary had no manual saves.\n return { version: 1, path: filePath, tool: tool as LegacyTimelineEvent['tool'], at };\n }\n if (version !== 2) return null;\n const origin = value.origin === 'scan' ? 'scan' : 'tool';\n return {\n version: 2,\n path: filePath,\n tool: tool as TimelineEvent['tool'],\n at,\n origin,\n ...(optionalString(value.before) === undefined ? {} : { before: value.before as string }),\n ...(optionalString(value.after) === undefined ? {} : { after: value.after as string }),\n ...(optionalCount(value.additions) === undefined ? {} : { additions: value.additions as number }),\n ...(optionalCount(value.removals) === undefined ? {} : { removals: value.removals as number }),\n ...(value.verified === true ? { verified: true } : {}),\n };\n}\n\n/** Every recorded line of the timeline, malformed ones dropped, in file order. */\nexport function parseTimeline(content: string, onMalformed?: (line: string) => void): AnyTimelineEvent[] {\n const events: AnyTimelineEvent[] = [];\n for (const line of content.split('\\n')) {\n if (line === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n onMalformed?.(line);\n continue;\n }\n const event = parseTimelineEvent(parsed);\n if (event === null) onMalformed?.(line);\n else events.push(event);\n }\n return events;\n}\n\n/**\n * Whether a recorded change is evidence that the file's content actually moved.\n *\n * A tool change names its file and reads both sides, so it always is. A scan\n * change is a tree comparison of size and modification time, which a checkout,\n * an install, or a command that merely touched a file moves as readily as an\n * edit does; only a scan the tracker confirmed carries `verified`. Version 1\n * lines predate the distinction entirely, so a bash one cannot be trusted.\n */\nexport function isConfirmedChange(event: AnyTimelineEvent): boolean {\n if (event.version !== 2) return event.tool !== 'bash';\n return event.origin !== 'scan' || event.verified === true;\n}\n\n/** The recorded changes worth listing, which is every change but an unconfirmed touch. */\nexport function confirmedChanges(events: readonly AnyTimelineEvent[]): AnyTimelineEvent[] {\n return events.filter(isConfirmedChange);\n}\n/** One row per file, newest change first, which is the order both docks list in. */\nexport function foldEntries(events: readonly AnyTimelineEvent[]): FileEditEntry[] {\n const folded = new Map<string, FileEditEntry>();\n for (const event of events) {\n const current = folded.get(event.path);\n folded.set(event.path, {\n path: event.path,\n tool: event.at >= (current?.at ?? 0) ? event.tool : (current?.tool ?? event.tool),\n at: Math.max(event.at, current?.at ?? 0),\n count: (current?.count ?? 0) + 1,\n });\n }\n return [...folded.values()].sort((left, right) => right.at - left.at);\n}\n\n/** One file's history, oldest first, numbered so a reader can name a version. */\nexport function foldVersions(events: readonly AnyTimelineEvent[], filePath: string): FileEditVersion[] {\n return events\n .filter((event) => event.path === filePath)\n .sort((left, right) => left.at - right.at)\n .map((event, offset) => {\n const version: FileEditVersion = {\n index: offset + 1,\n tool: event.tool,\n at: event.at,\n origin: event.version === 2 ? event.origin : 'scan',\n };\n if (event.version !== 2) return version;\n return {\n ...version,\n ...(event.before === undefined ? {} : { before: event.before }),\n ...(event.after === undefined ? {} : { after: event.after }),\n ...(event.additions === undefined ? {} : { additions: event.additions }),\n ...(event.removals === undefined ? {} : { removals: event.removals }),\n ...(event.verified === true ? { verified: true } : {}),\n };\n });\n}\n\n/**\n * Whether a file can be diffed at all: some version has to have captured what\n * the file held before it changed. A file only ever seen after the fact, which\n * is every file a bash script wrote, never can be.\n */\nexport function isDiffable(versions: readonly FileEditVersion[]): boolean {\n return versions.some((version) => version.before !== undefined);\n}\n\n/** The oldest captured baseline for a file, which is what the whole-session diff starts from. */\nexport function baselineOf(versions: readonly FileEditVersion[]): string | undefined {\n return versions.find((version) => version.before !== undefined)?.before;\n}\n"],"mappings":"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"}
@@ -29,6 +29,12 @@ interface TimelineEvent {
29
29
  after?: string;
30
30
  additions?: number;
31
31
  removals?: number;
32
+ /**
33
+ * Set on a scan change whose content was proven to have moved, rather than
34
+ * only its modification time. Absent on a scan recorded by an older build,
35
+ * which could not tell an edit from a file a command merely touched.
36
+ */
37
+ verified?: boolean;
32
38
  }
33
39
  /** A version 1 event, still accepted from a timeline opened by an older build. */
34
40
  interface LegacyTimelineEvent {
@@ -54,6 +60,7 @@ interface FileEditVersion {
54
60
  after?: string;
55
61
  additions?: number;
56
62
  removals?: number;
63
+ verified?: boolean;
57
64
  }
58
65
  interface FileDiff {
59
66
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.cts","names":[],"sources":["../../src/types/domain.ts"],"mappings":";KAAY;KACA;;;;;;;;;KAUA;;;;;;;;UASK;EACf;EACA;EACA,MAAM;EACN;EACA,QAAQ;;EAER;;EAEA;EACA;EACA;;;UAIe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA,MAAM;EACN;EACA;;;UAIe;;EAEf;EACA,MAAM;EACN;EACA,QAAQ;EACR;EACA;EACA;EACA;;UAGe;EACf;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA"}
1
+ {"version":3,"file":"domain.d.cts","names":[],"sources":["../../src/types/domain.ts"],"mappings":";KAAY;KACA;;;;;;;;;KAUA;;;;;;;;UASK;EACf;EACA;EACA,MAAM;EACN;EACA,QAAQ;;EAER;;EAEA;EACA;EACA;;;;;;EAMA;;;UAIe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA,MAAM;EACN;EACA;;;UAIe;;EAEf;EACA,MAAM;EACN;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA"}
@@ -29,6 +29,12 @@ interface TimelineEvent {
29
29
  after?: string;
30
30
  additions?: number;
31
31
  removals?: number;
32
+ /**
33
+ * Set on a scan change whose content was proven to have moved, rather than
34
+ * only its modification time. Absent on a scan recorded by an older build,
35
+ * which could not tell an edit from a file a command merely touched.
36
+ */
37
+ verified?: boolean;
32
38
  }
33
39
  /** A version 1 event, still accepted from a timeline opened by an older build. */
34
40
  interface LegacyTimelineEvent {
@@ -54,6 +60,7 @@ interface FileEditVersion {
54
60
  after?: string;
55
61
  additions?: number;
56
62
  removals?: number;
63
+ verified?: boolean;
57
64
  }
58
65
  interface FileDiff {
59
66
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.mts","names":[],"sources":["../../src/types/domain.ts"],"mappings":";KAAY;KACA;;;;;;;;;KAUA;;;;;;;;UASK;EACf;EACA;EACA,MAAM;EACN;EACA,QAAQ;;EAER;;EAEA;EACA;EACA;;;UAIe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA,MAAM;EACN;EACA;;;UAIe;;EAEf;EACA,MAAM;EACN;EACA,QAAQ;EACR;EACA;EACA;EACA;;UAGe;EACf;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA"}
1
+ {"version":3,"file":"domain.d.mts","names":[],"sources":["../../src/types/domain.ts"],"mappings":";KAAY;KACA;;;;;;;;;KAUA;;;;;;;;UASK;EACf;EACA;EACA,MAAM;EACN;EACA,QAAQ;;EAER;;EAEA;EACA;EACA;;;;;;EAMA;;;UAIe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA,MAAM;EACN;EACA;;;UAIe;;EAEf;EACA,MAAM;EACN;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-file-edit",
3
- "version": "0.0.1-alpha.43",
3
+ "version": "0.0.1-alpha.45",
4
4
  "description": "Pi extension for session-scoped file-change timelines and external editor workflows.",
5
5
  "keywords": [
6
6
  "ai",
@@ -67,15 +67,15 @@
67
67
  "dependencies": {
68
68
  "@deepseek-ai/cordis": "4.0.1",
69
69
  "cross-spawn": "7.0.6",
70
- "hono": "4.13.3",
70
+ "hono": "4.13.5",
71
71
  "ignore": "7.0.6",
72
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.43",
73
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.43",
74
- "@agimon-ai/doompi-ui": "0.0.1-alpha.43",
75
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.6",
76
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.4",
77
- "@agimon-ai/doompi-web-security": "0.0.1-alpha.5",
78
- "@agimon-ai/doompi-config": "0.0.1-alpha.43"
72
+ "@agimon-ai/doompi-config": "0.0.1-alpha.44",
73
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.44",
74
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.5",
75
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.45",
76
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.44",
77
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.8",
78
+ "@agimon-ai/doompi-web-security": "0.0.1-alpha.6"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@earendil-works/pi-coding-agent": "0.84.4",
@@ -83,9 +83,9 @@
83
83
  "@tanstack/react-store": "0.11.1",
84
84
  "@tanstack/store": "0.11.1",
85
85
  "@types/cross-spawn": "6.0.6",
86
- "@types/node": "26.2.0",
87
- "@types/react": "19.2.7",
88
- "@types/react-dom": "19.2.3",
86
+ "@types/node": "26.4.0",
87
+ "@types/react": "19.2.18",
88
+ "@types/react-dom": "19.2.5",
89
89
  "@vitest/coverage-v8": "4.1.11",
90
90
  "react": "19.2.8",
91
91
  "react-dom": "19.2.8",
@@ -30,6 +30,12 @@ export interface TimelineEvent {
30
30
  after?: string;
31
31
  additions?: number;
32
32
  removals?: number;
33
+ /**
34
+ * Set on a scan change whose content was proven to have moved, rather than
35
+ * only its modification time. Absent on a scan recorded by an older build,
36
+ * which could not tell an edit from a file a command merely touched.
37
+ */
38
+ verified?: boolean;
33
39
  }
34
40
 
35
41
  /** A version 1 event, still accepted from a timeline opened by an older build. */
@@ -58,6 +64,7 @@ export interface FileEditVersion {
58
64
  after?: string;
59
65
  additions?: number;
60
66
  removals?: number;
67
+ verified?: boolean;
61
68
  }
62
69
 
63
70
  export interface FileDiff {
@@ -0,0 +1,45 @@
1
+ import type { FileLinkSource, TransientTab } from '@agimon-ai/doompi-web-contracts';
2
+ import type { FilesItemView } from '../src/types/webFiles.ts';
3
+ import { fileTab } from './FilePanel.tsx';
4
+ import { files } from './filesStore.ts';
5
+
6
+ /**
7
+ * The files a message names, as links into the same tab the activity dock
8
+ * opens.
9
+ *
10
+ * Only files this session recorded a change to are claimed. A message quotes a
11
+ * great deal that is shaped like nothing in particular, class names and globs
12
+ * and flags among it, and guessing at what is a path produces links that open
13
+ * an error. The recorded set is the one thing the page knows for certain, and
14
+ * it is also the set a reader is most likely to want open.
15
+ */
16
+
17
+ /** A reference the way a message writes one: 'src/app.ts:42' or 'src/app.ts:42:9'. */
18
+ const LINE_SUFFIX = /:\d+(?::\d+)?$/;
19
+
20
+ function itemsFor(sessionId: string | null): readonly FilesItemView[] {
21
+ return files.select(files.store.state, sessionId).items;
22
+ }
23
+
24
+ function match(sessionId: string | null, path: string): FilesItemView | undefined {
25
+ const candidate = path.trim().replace(LINE_SUFFIX, '');
26
+ if (candidate === '') return undefined;
27
+ return itemsFor(sessionId).find((item) => item.relPath === candidate || item.path === candidate);
28
+ }
29
+
30
+ export const fileLinks: FileLinkSource = {
31
+ subscribe(listener: () => void) {
32
+ const subscription = files.store.subscribe(listener);
33
+ return () => subscription.unsubscribe();
34
+ },
35
+ // The paths themselves, not their count: a file that leaves the list while
36
+ // another arrives changes what resolves without changing how many do.
37
+ fingerprint: (sessionId) =>
38
+ itemsFor(sessionId)
39
+ .map((item) => item.relPath)
40
+ .join('\n'),
41
+ resolve: (sessionId, path): TransientTab | undefined => {
42
+ const item = match(sessionId, path);
43
+ return item === undefined ? undefined : fileTab(item.path, item.relPath);
44
+ },
45
+ };
package/web/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { defineWebPlugin } from '@agimon-ai/doompi-web-contracts';
2
+ import { fileLinks } from './fileLinks.ts';
2
3
  import { FilesActivitySection } from './FilesActivitySection.tsx';
3
4
  import { filesChannel } from './filesStore.ts';
4
5
  import { filesStatusKey } from '../src/types/webFiles.ts';
@@ -37,6 +38,9 @@ export const webPlugin = defineWebPlugin({
37
38
  // Same name as the group: the dock renders this inside it, in place of the
38
39
  // session's one-line summary.
39
40
  activitySections: [{ id: 'files', component: FilesActivitySection }],
41
+ // The same files, where a message names one: the dock is not the only place
42
+ // a reader meets a path.
43
+ fileLinks,
40
44
  leaderBindings: [
41
45
  {
42
46
  id: 'file-edit.files',