@agimon-ai/doompi-file-edit 0.0.1-alpha.83 → 0.0.1-alpha.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/server.cjs +2 -2
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +2 -2
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +2 -2
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +8 -8
|
@@ -606,10 +606,10 @@ var tool_execution_start_server_default = (0, _agimon_ai_doompi_core_extensionFi
|
|
|
606
606
|
//#endregion
|
|
607
607
|
//#region generated/server.ts
|
|
608
608
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
609
|
-
const via = (identity, value) =>
|
|
609
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
610
610
|
...identity,
|
|
611
611
|
...value
|
|
612
|
-
}
|
|
612
|
+
};
|
|
613
613
|
const facet = (0, _agimon_ai_doompi_core_serverFacet.defineServerPlugin)({
|
|
614
614
|
name: "@agimon-ai/doompi-file-edit",
|
|
615
615
|
global: () => ({ get channels() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.cjs","names":["defineApiRoutes","apiResponse","createHash","fs","path","lineDiffFromEmpty","lineDiff","baselineOf","TimelineStore","NodeSnapshotStoreAdapter","FileEditPaths","Hono","routes","fs","confirmedChanges","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","filesChannelType","FileEditPaths","createDoomIgnoreMatcher","fs","path","FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","EditTracker","NodeTreeManifestAdapter","NodeGitStatusAdapter","filesChannelType","filesStatusKey","defineRoot","defineChannel","defineRoute","defineHook","defineHook","defineServerPlugin","channelFileEdits","rootSession","composeRootHooks","apiRoute","hookToolExecutionEnd","hookToolExecutionStart"],"sources":["../../src/types/apiRoutes.ts","../../src/types/fileEditsApi.ts","../../src/services/fileEditsApi/index.ts","../../src/services/webFilesChannel/index.ts","../../src/services/fileEditSession/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/extensions/(backend)/channel/fileEdits.server.ts","../../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts","../../generated/server.ts"],"sourcesContent":["import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\n\nimport { baselineOf } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { lineDiff, lineDiffFromEmpty } from '../../services/lineDiff';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport routes from '../../types/apiRoutes';\nimport type { FileEditVersion } from '../../types/domain';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsPreviewView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../../types/fileEditsApi';\nimport type { SnapshotStorePort } from '../../types/snapshotStore';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\n/**\n * This package's HTTP surface: one file's history, and the manual save.\n *\n * Routes are relative to the mount its host gives it, so nothing here repeats\n * where it was mounted. The host is one session's own server, which is what\n * makes the session id and working directory available without the page\n * supplying either: a page names a file, never a session's private paths.\n *\n * The detail route answers everything a reader needs to open a changed file,\n * because splitting it would make the page stitch three responses together for\n * one click. The save route is the only mutation, and it refuses unless the\n * reader proves they were looking at the content that is still on disk. The\n * preview route is the read-only way in to a file the session never changed,\n * bounded by the working directory instead of by the timeline.\n */\n\n/** Past this, the working view reports the file rather than carrying it. */\nexport const MAX_WORKING_BYTES = 1024 * 1024;\n\nconst BINARY_SAMPLE_BYTES = 8192;\nconst NO_BASELINE_NOTE = 'changed by a command, so no baseline was captured; showing the file as it stands';\n/**\n * A change the tool layer named but never captured content for. That is what\n * every record written before this package learned to snapshot looks like, and\n * calling those \"changed by a command\" would be wrong: the tool is right there\n * in the record.\n */\nconst NO_SNAPSHOT_NOTE = 'recorded before this session began capturing content, so there is no diff for it';\nconst NO_AFTER_NOTE = 'the new content could not be captured: the file is binary or past the size cap';\nconst NO_BEFORE_NOTE = 'the previous content could not be captured: it was binary or past the size cap';\nconst APPROXIMATE_NOTE = 'the changed region was too large to match line by line';\n\nexport interface FileEditsApiOptions {\n /** The session these routes answer for; absent when the host is the hub. */\n sessionId?: string;\n /** The session's working directory, which relative paths are shown against. */\n cwd?: string;\n timeline?: ITimelineStore;\n snapshots?: SnapshotStorePort;\n paths?: IFileEditPaths;\n}\n\nfunction hashOf(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Reads the file as the source view will show it, or says why it cannot. */\nasync function readWorking(filePath: string): Promise<FileEditsWorkingView> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch {\n return { content: '', hash: '', unavailable: true, reason: 'the file no longer exists' };\n }\n if (raw.byteLength > MAX_WORKING_BYTES) {\n return { content: '', hash: '', unavailable: true, reason: `the file is larger than ${MAX_WORKING_BYTES} bytes` };\n }\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n return { content: '', hash: '', unavailable: true, reason: 'the file is binary' };\n }\n const content = raw.toString('utf8');\n return { content, hash: hashOf(content), unavailable: false };\n}\n\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * The same containment check after symlinks are followed. A path that does not\n * exist yet resolves to nothing, and there is nothing to hand out, so it\n * passes and the read below reports it missing.\n */\nasync function isRealPathInside(root: string, candidate: string): Promise<boolean> {\n let realRoot: string;\n let real: string;\n try {\n realRoot = await fs.realpath(root);\n real = await fs.realpath(candidate);\n } catch {\n return true;\n }\n return isInside(realRoot, real);\n}\n\n/**\n * One recorded change as the page renders it. A change with both sides captured\n * carries its own diff; one without says why, rather than showing nothing.\n *\n * A file this session created counts as captured on both sides. Its baseline is\n * empty rather than missing, so the whole file reads as arriving at once, which\n * is exactly what happened.\n */\nasync function toVersionView(version: FileEditVersion, snapshots: SnapshotStorePort): Promise<FileEditsVersionView> {\n const view: FileEditsVersionView = {\n index: version.index,\n tool: version.tool,\n at: version.at,\n origin: version.origin,\n additions: version.additions ?? 0,\n removals: version.removals ?? 0,\n };\n const createdHere = version.before === undefined && version.created === true;\n if (version.after === undefined || (version.before === undefined && !createdHere)) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but content was not stored: either it did not fit, or\n // the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: version.after === undefined ? NO_AFTER_NOTE : NO_BEFORE_NOTE };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const after = await snapshots.read(version.after);\n if (after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n if (version.before === undefined) {\n const fresh = lineDiffFromEmpty(after);\n return {\n ...view,\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(version.before);\n if (before === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n const diff = lineDiff(before, after);\n return {\n ...view,\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\n/** Everything the session did to the file: its oldest baseline against what is on disk now. */\nasync function toCumulativeView(\n versions: readonly FileEditVersion[],\n working: FileEditsWorkingView,\n snapshots: SnapshotStorePort,\n): Promise<FileEditsCumulativeView> {\n // A file this session created starts from nothing, and nothing is a baseline\n // the snapshot store never has to hold.\n const createdHere = versions[0]?.created === true;\n const baseline = baselineOf(versions);\n if (!createdHere && baseline === undefined) {\n return {\n additions: 0,\n removals: 0,\n note: versions[0]?.origin === 'tool' ? NO_BEFORE_NOTE : NO_BASELINE_NOTE,\n };\n }\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n if (createdHere || baseline === undefined) {\n const fresh = lineDiffFromEmpty(working.content);\n return {\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(baseline);\n if (before === undefined) {\n return { additions: 0, removals: 0, note: 'the content this session started from is no longer held' };\n }\n const diff = lineDiff(before, working.content);\n return {\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\nexport function createFileEditsApi(options: FileEditsApiOptions = {}): Hono {\n const timeline = options.timeline ?? new TimelineStore();\n const snapshots = options.snapshots ?? new NodeSnapshotStoreAdapter();\n const paths = options.paths ?? new FileEditPaths();\n const cwd = options.cwd ?? '';\n const app = new Hono();\n\n // The routes and the extension run in different processes, so this half\n // opens the same two locations from the same inputs rather than sharing an\n // instance with the half that writes them.\n //\n // Only what this function built gets pointed at them. A store handed in\n // through the options arrives already configured, and repointing it would\n // make the option a lie: the caller would be injecting a store the routes\n // then read somewhere else entirely.\n if (options.sessionId !== undefined && cwd !== '') {\n const sessionKey = paths.sessionKey(options.sessionId);\n if (options.timeline === undefined) timeline.initialize(paths.timelinePath(cwd, sessionKey));\n if (options.snapshots === undefined) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));\n }\n\n /**\n * The file a request names, rejected unless this session actually recorded a\n * change to it. That check is the authorization: these routes read and write\n * whatever path they are handed, so the timeline is what bounds them to files\n * the session already touched.\n */\n const recordedVersions = async (filePath: string | undefined): Promise<FileEditVersion[] | undefined> => {\n if (filePath === undefined || filePath === '') return undefined;\n const versions = await timeline.versions(path.resolve(cwd, filePath));\n return versions.length === 0 ? undefined : versions;\n };\n\n app.get(routes.detail.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n const versions = await recordedVersions(requested);\n if (versions === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n const working = await readWorking(filePath);\n const body: FileEditsDetailView = {\n path: filePath,\n relPath: cwd === '' ? filePath : path.relative(cwd, filePath),\n versions: await Promise.all(versions.map((version) => toVersionView(version, snapshots))),\n cumulative: await toCumulativeView(versions, working, snapshots),\n working,\n };\n return context.json(body);\n });\n\n /**\n * The file a request names, read only, for a file the timeline knows nothing\n * about: a path the agent read but never changed is still a path the reader\n * clicked, and refusing it would leave the link dead.\n *\n * The working directory is the boundary here, since the timeline is not.\n * Containment is checked lexically and again on the real path, so a symlink\n * inside the tree cannot hand out something beyond it, and a host that gave\n * these routes no working directory has no boundary to enforce and so serves\n * nothing.\n */\n app.get(routes.preview.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if (requested === undefined || requested === '' || requested.includes('\\0')) {\n return context.json({ error: 'A preview names a path.' }, 400);\n }\n if (cwd === '') return context.json({ error: 'This session has no working directory to read from.' }, 403);\n const root = path.resolve(cwd);\n const filePath = path.resolve(root, requested);\n if (!isInside(root, filePath) || !(await isRealPathInside(root, filePath))) {\n return context.json({ error: 'The path leaves the session directory.' }, 403);\n }\n const body: FileEditsPreviewView = {\n path: filePath,\n relPath: path.relative(root, filePath),\n working: await readWorking(filePath),\n };\n return context.json(body);\n });\n\n app.put(routes.save.path, async (context) => {\n let request: FileEditsSaveRequest;\n try {\n request = (await context.req.json()) as FileEditsSaveRequest;\n } catch {\n return context.json({ error: 'The save body is not JSON.' }, 400);\n }\n if (typeof request.path !== 'string' || typeof request.content !== 'string') {\n return context.json({ error: 'A save names a path and the content to write.' }, 400);\n }\n if ((await recordedVersions(request.path)) === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, request.path);\n const current = await readWorking(filePath);\n if (current.unavailable) {\n return context.json({ error: current.reason ?? 'The file cannot be read.' }, 409);\n }\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. Refusing on a moved hash is what\n // stops the save from silently discarding whatever it did.\n if (current.hash !== request.expectedHash) {\n return context.json({ error: 'The file changed since it was opened.', hash: current.hash }, 409);\n }\n await fs.writeFile(filePath, request.content, 'utf8');\n const body: FileEditsSaveView = { hash: hashOf(request.content) };\n return context.json(body);\n });\n\n app.delete(routes.remove.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if ((await recordedVersions(requested)) === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n try {\n await fs.unlink(filePath);\n } catch (error) {\n // Already gone is the outcome the caller wanted, so it is not a failure.\n if (!hasCode(error, 'ENOENT')) {\n return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);\n }\n }\n return context.body(null, 204);\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built entry. */\nexport const api: DoomApi = {\n basePath: API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n const app = createFileEditsApi({\n ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),\n ...(context.cwd === undefined ? {} : { cwd: context.cwd }),\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request here: every route reads, answers and closes.\n close: () => undefined,\n };\n },\n};\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { filterDoomIgnoredFiles } from '../../services/doomIgnore';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { filesChannelType, type FilesItemView } from '../../types/webFiles';\n\n/**\n * The changed-files channel consumes snapshots emitted by the owning session\n * facet. Durable timeline reads seed a channel that starts after session startup.\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\nfunction filesPayload(value: unknown): value is { items: FilesItemView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { items?: unknown }).items);\n}\n\n/**\n * The files data channel: one direct-event subscription per managed session,\n * published as { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(): DoomHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const subscriptions = new Map<string, () => void>();\n const paths = new FileEditPaths();\n const channelSource: DoomHubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n let timelinePath: string | undefined;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // The session has no readable state path yet.\n }\n const items = timelinePath === undefined ? [] : readSessionFiles(timelinePath, scope.cwd);\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n const unsubscribe = host.directEvents.subscribe(\n filesChannelType,\n scope.sessionId,\n (payload) => {\n if (!filesPayload(payload)) return;\n latest.set(scope.sessionId, payload.items);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n type DoomHeadlessExecutionContext,\n type DoomHeadlessActivity,\n type DoomHeadlessHook,\n} from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerPluginContext, DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createDoomIgnoreMatcher } from '../../services/doomIgnore';\nimport { EditTracker } from '../../services/editTracker';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { NodeGitStatusAdapter } from '../../services/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport { NodeTreeManifestAdapter } from '../../services/treeManifest';\nimport { filesChannelType, filesStatusKey } from '../../types/webFiles';\nimport { api } from '../fileEditsApi';\nimport { readSessionFiles } from '../webFilesChannel';\n\nconst DOOM_IGNORE_FILE = '.doomignore';\n\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n try {\n const matcher = createDoomIgnoreMatcher(fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8'));\n return matcher === undefined ? undefined : (filePath: string) => matcher(path.relative(cwd, filePath));\n } catch {\n return undefined;\n }\n}\n\nfunction stringValue(value: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const candidate = value[key];\n return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;\n}\n\nexport function createFileEditSession({ agent, host }: DoomServerPluginContext): DoomServerSessionPlugin {\n if (!agent) return { api: [api] };\n const paths = new FileEditPaths();\n const timeline = new TimelineStore();\n const snapshots = new NodeSnapshotStoreAdapter();\n const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), {\n git: new NodeGitStatusAdapter(),\n });\n if (host.context.directEvents === undefined)\n throw new Error('File-edit headless facet requires the session direct event bus.');\n const directEvents = host.context.directEvents;\n let active:\n | {\n readonly context: DoomHeadlessExecutionContext;\n readonly timelinePath: string;\n readonly snapshotsPath: string;\n }\n | undefined;\n /**\n * The channel carries the rows; the status decides whether the group exists.\n *\n * A cockpit drives this facet and never the Pi runtime, so without the status\n * the dock had the file list on the wire and no frame to draw it in. The empty\n * string is deliberate: the group declares `hideWhenEmpty`, so a session that\n * has edited nothing publishes a key with no content and stays hidden.\n */\n const publish = (runtime: NonNullable<typeof active>): void => {\n const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);\n directEvents.publish(filesChannelType, runtime.context.sessionId, { items });\n runtime.context.client.setStatus(\n filesStatusKey,\n items.length === 0 ? '' : `${items.length} ${items.length === 1 ? 'file' : 'files'}`,\n );\n };\n const activity: DoomHeadlessActivity = {\n name: 'file-edits',\n start: async (executionContext) => {\n const sessionKey = paths.sessionKey(executionContext.sessionId);\n const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n const isIgnored = readDoomIgnore(executionContext.cwd);\n editTracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n const runtime = { context: executionContext, timelinePath, snapshotsPath };\n active = runtime;\n publish(runtime);\n return async () => {\n if (active !== runtime) return;\n active = undefined;\n executionContext.client.setStatus(filesStatusKey, undefined);\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_execution_start',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n const toolName = stringValue(event, 'toolName');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId || !toolName)\n return;\n await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);\n },\n };\n const endHook: DoomHeadlessHook = {\n event: 'tool_execution_end',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId) return;\n await editTracker.end(toolCallId, event.isError === true, executionContext.cwd);\n if (active === runtime) publish(runtime);\n },\n };\n return { api: [api], activities: [activity], hooks: [startHook, endHook] };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createFileEditSession } from '../../../../../services/fileEditSession';\nexport const createFileEditServerRoot = (context: DoomServerPluginContext) => {\n const runtime = createFileEditSession(context);\n return { value: runtime, activities: runtime.activities };\n};\nexport type FileEditServerScope = Awaited<ReturnType<typeof createFileEditServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createFileEditServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createFileEditServerRoot);\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createFilesChannel } from '../../../services/webFilesChannel';\nexport default defineChannel(function fileEditsChannel(): DoomHubChannel {\n return createFilesChannel();\n});\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/fileEditsApi';\n\n/**\n * Mounts this package's HTTP API at `file-edits`, which the folder names.\n *\n * Until this file existed the API was never mounted at all. The session root\n * returns a runtime carrying `api`, but a root's `api` is not folded into the\n * generated entry the way its services and activities are, so the registration\n * was dropped in silence and the cockpit called routes that answered 404.\n */\nexport default defineRoute(api);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![1]!);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![0]!);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelFileEdits from '../src/extensions/(backend)/channel/fileEdits.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server';\nimport hookToolExecutionEnd from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server';\nimport hookToolExecutionStart from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T => ({ ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-file-edit',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n get hooks(): DoomServerSessionPlugin['hooks'] { return [via({ event: 'tool_execution_end' }, at(hookToolExecutionEnd, contextSession)), via({ event: 'tool_execution_start' }, at(hookToolExecutionStart, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.cjs","names":["defineApiRoutes","apiResponse","createHash","fs","path","lineDiffFromEmpty","lineDiff","baselineOf","TimelineStore","NodeSnapshotStoreAdapter","FileEditPaths","Hono","routes","fs","confirmedChanges","parseTimeline","foldEntries","path","isDiffable","foldVersions","filterDoomIgnoredFiles","filesChannelType","FileEditPaths","createDoomIgnoreMatcher","fs","path","FileEditPaths","TimelineStore","NodeSnapshotStoreAdapter","EditTracker","NodeTreeManifestAdapter","NodeGitStatusAdapter","filesChannelType","filesStatusKey","defineRoot","defineChannel","defineRoute","defineHook","defineHook","defineServerPlugin","channelFileEdits","rootSession","composeRootHooks","apiRoute","hookToolExecutionEnd","hookToolExecutionStart"],"sources":["../../src/types/apiRoutes.ts","../../src/types/fileEditsApi.ts","../../src/services/fileEditsApi/index.ts","../../src/services/webFilesChannel/index.ts","../../src/services/fileEditSession/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/extensions/(backend)/channel/fileEdits.server.ts","../../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts","../../generated/server.ts"],"sourcesContent":["import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\n\nimport { baselineOf } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { lineDiff, lineDiffFromEmpty } from '../../services/lineDiff';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport routes from '../../types/apiRoutes';\nimport type { FileEditVersion } from '../../types/domain';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsPreviewView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../../types/fileEditsApi';\nimport type { SnapshotStorePort } from '../../types/snapshotStore';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\n/**\n * This package's HTTP surface: one file's history, and the manual save.\n *\n * Routes are relative to the mount its host gives it, so nothing here repeats\n * where it was mounted. The host is one session's own server, which is what\n * makes the session id and working directory available without the page\n * supplying either: a page names a file, never a session's private paths.\n *\n * The detail route answers everything a reader needs to open a changed file,\n * because splitting it would make the page stitch three responses together for\n * one click. The save route is the only mutation, and it refuses unless the\n * reader proves they were looking at the content that is still on disk. The\n * preview route is the read-only way in to a file the session never changed,\n * bounded by the working directory instead of by the timeline.\n */\n\n/** Past this, the working view reports the file rather than carrying it. */\nexport const MAX_WORKING_BYTES = 1024 * 1024;\n\nconst BINARY_SAMPLE_BYTES = 8192;\nconst NO_BASELINE_NOTE = 'changed by a command, so no baseline was captured; showing the file as it stands';\n/**\n * A change the tool layer named but never captured content for. That is what\n * every record written before this package learned to snapshot looks like, and\n * calling those \"changed by a command\" would be wrong: the tool is right there\n * in the record.\n */\nconst NO_SNAPSHOT_NOTE = 'recorded before this session began capturing content, so there is no diff for it';\nconst NO_AFTER_NOTE = 'the new content could not be captured: the file is binary or past the size cap';\nconst NO_BEFORE_NOTE = 'the previous content could not be captured: it was binary or past the size cap';\nconst APPROXIMATE_NOTE = 'the changed region was too large to match line by line';\n\nexport interface FileEditsApiOptions {\n /** The session these routes answer for; absent when the host is the hub. */\n sessionId?: string;\n /** The session's working directory, which relative paths are shown against. */\n cwd?: string;\n timeline?: ITimelineStore;\n snapshots?: SnapshotStorePort;\n paths?: IFileEditPaths;\n}\n\nfunction hashOf(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Reads the file as the source view will show it, or says why it cannot. */\nasync function readWorking(filePath: string): Promise<FileEditsWorkingView> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch {\n return { content: '', hash: '', unavailable: true, reason: 'the file no longer exists' };\n }\n if (raw.byteLength > MAX_WORKING_BYTES) {\n return { content: '', hash: '', unavailable: true, reason: `the file is larger than ${MAX_WORKING_BYTES} bytes` };\n }\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n return { content: '', hash: '', unavailable: true, reason: 'the file is binary' };\n }\n const content = raw.toString('utf8');\n return { content, hash: hashOf(content), unavailable: false };\n}\n\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * The same containment check after symlinks are followed. A path that does not\n * exist yet resolves to nothing, and there is nothing to hand out, so it\n * passes and the read below reports it missing.\n */\nasync function isRealPathInside(root: string, candidate: string): Promise<boolean> {\n let realRoot: string;\n let real: string;\n try {\n realRoot = await fs.realpath(root);\n real = await fs.realpath(candidate);\n } catch {\n return true;\n }\n return isInside(realRoot, real);\n}\n\n/**\n * One recorded change as the page renders it. A change with both sides captured\n * carries its own diff; one without says why, rather than showing nothing.\n *\n * A file this session created counts as captured on both sides. Its baseline is\n * empty rather than missing, so the whole file reads as arriving at once, which\n * is exactly what happened.\n */\nasync function toVersionView(version: FileEditVersion, snapshots: SnapshotStorePort): Promise<FileEditsVersionView> {\n const view: FileEditsVersionView = {\n index: version.index,\n tool: version.tool,\n at: version.at,\n origin: version.origin,\n additions: version.additions ?? 0,\n removals: version.removals ?? 0,\n };\n const createdHere = version.before === undefined && version.created === true;\n if (version.after === undefined || (version.before === undefined && !createdHere)) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but content was not stored: either it did not fit, or\n // the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: version.after === undefined ? NO_AFTER_NOTE : NO_BEFORE_NOTE };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const after = await snapshots.read(version.after);\n if (after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n if (version.before === undefined) {\n const fresh = lineDiffFromEmpty(after);\n return {\n ...view,\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(version.before);\n if (before === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n const diff = lineDiff(before, after);\n return {\n ...view,\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\n/** Everything the session did to the file: its oldest baseline against what is on disk now. */\nasync function toCumulativeView(\n versions: readonly FileEditVersion[],\n working: FileEditsWorkingView,\n snapshots: SnapshotStorePort,\n): Promise<FileEditsCumulativeView> {\n // A file this session created starts from nothing, and nothing is a baseline\n // the snapshot store never has to hold.\n const createdHere = versions[0]?.created === true;\n const baseline = baselineOf(versions);\n if (!createdHere && baseline === undefined) {\n return {\n additions: 0,\n removals: 0,\n note: versions[0]?.origin === 'tool' ? NO_BEFORE_NOTE : NO_BASELINE_NOTE,\n };\n }\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n if (createdHere || baseline === undefined) {\n const fresh = lineDiffFromEmpty(working.content);\n return {\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(baseline);\n if (before === undefined) {\n return { additions: 0, removals: 0, note: 'the content this session started from is no longer held' };\n }\n const diff = lineDiff(before, working.content);\n return {\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\nexport function createFileEditsApi(options: FileEditsApiOptions = {}): Hono {\n const timeline = options.timeline ?? new TimelineStore();\n const snapshots = options.snapshots ?? new NodeSnapshotStoreAdapter();\n const paths = options.paths ?? new FileEditPaths();\n const cwd = options.cwd ?? '';\n const app = new Hono();\n\n // The routes and the extension run in different processes, so this half\n // opens the same two locations from the same inputs rather than sharing an\n // instance with the half that writes them.\n //\n // Only what this function built gets pointed at them. A store handed in\n // through the options arrives already configured, and repointing it would\n // make the option a lie: the caller would be injecting a store the routes\n // then read somewhere else entirely.\n if (options.sessionId !== undefined && cwd !== '') {\n const sessionKey = paths.sessionKey(options.sessionId);\n if (options.timeline === undefined) timeline.initialize(paths.timelinePath(cwd, sessionKey));\n if (options.snapshots === undefined) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));\n }\n\n /**\n * The file a request names, rejected unless this session actually recorded a\n * change to it. That check is the authorization: these routes read and write\n * whatever path they are handed, so the timeline is what bounds them to files\n * the session already touched.\n */\n const recordedVersions = async (filePath: string | undefined): Promise<FileEditVersion[] | undefined> => {\n if (filePath === undefined || filePath === '') return undefined;\n const versions = await timeline.versions(path.resolve(cwd, filePath));\n return versions.length === 0 ? undefined : versions;\n };\n\n app.get(routes.detail.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n const versions = await recordedVersions(requested);\n if (versions === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n const working = await readWorking(filePath);\n const body: FileEditsDetailView = {\n path: filePath,\n relPath: cwd === '' ? filePath : path.relative(cwd, filePath),\n versions: await Promise.all(versions.map((version) => toVersionView(version, snapshots))),\n cumulative: await toCumulativeView(versions, working, snapshots),\n working,\n };\n return context.json(body);\n });\n\n /**\n * The file a request names, read only, for a file the timeline knows nothing\n * about: a path the agent read but never changed is still a path the reader\n * clicked, and refusing it would leave the link dead.\n *\n * The working directory is the boundary here, since the timeline is not.\n * Containment is checked lexically and again on the real path, so a symlink\n * inside the tree cannot hand out something beyond it, and a host that gave\n * these routes no working directory has no boundary to enforce and so serves\n * nothing.\n */\n app.get(routes.preview.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if (requested === undefined || requested === '' || requested.includes('\\0')) {\n return context.json({ error: 'A preview names a path.' }, 400);\n }\n if (cwd === '') return context.json({ error: 'This session has no working directory to read from.' }, 403);\n const root = path.resolve(cwd);\n const filePath = path.resolve(root, requested);\n if (!isInside(root, filePath) || !(await isRealPathInside(root, filePath))) {\n return context.json({ error: 'The path leaves the session directory.' }, 403);\n }\n const body: FileEditsPreviewView = {\n path: filePath,\n relPath: path.relative(root, filePath),\n working: await readWorking(filePath),\n };\n return context.json(body);\n });\n\n app.put(routes.save.path, async (context) => {\n let request: FileEditsSaveRequest;\n try {\n request = (await context.req.json()) as FileEditsSaveRequest;\n } catch {\n return context.json({ error: 'The save body is not JSON.' }, 400);\n }\n if (typeof request.path !== 'string' || typeof request.content !== 'string') {\n return context.json({ error: 'A save names a path and the content to write.' }, 400);\n }\n if ((await recordedVersions(request.path)) === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, request.path);\n const current = await readWorking(filePath);\n if (current.unavailable) {\n return context.json({ error: current.reason ?? 'The file cannot be read.' }, 409);\n }\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. Refusing on a moved hash is what\n // stops the save from silently discarding whatever it did.\n if (current.hash !== request.expectedHash) {\n return context.json({ error: 'The file changed since it was opened.', hash: current.hash }, 409);\n }\n await fs.writeFile(filePath, request.content, 'utf8');\n const body: FileEditsSaveView = { hash: hashOf(request.content) };\n return context.json(body);\n });\n\n app.delete(routes.remove.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if ((await recordedVersions(requested)) === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n try {\n await fs.unlink(filePath);\n } catch (error) {\n // Already gone is the outcome the caller wanted, so it is not a failure.\n if (!hasCode(error, 'ENOENT')) {\n return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);\n }\n }\n return context.body(null, 204);\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built entry. */\nexport const api: DoomApi = {\n basePath: API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n const app = createFileEditsApi({\n ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),\n ...(context.cwd === undefined ? {} : { cwd: context.cwd }),\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request here: every route reads, answers and closes.\n close: () => undefined,\n };\n },\n};\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { filterDoomIgnoredFiles } from '../../services/doomIgnore';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { filesChannelType, type FilesItemView } from '../../types/webFiles';\n\n/**\n * The changed-files channel consumes snapshots emitted by the owning session\n * facet. Durable timeline reads seed a channel that starts after session startup.\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\nfunction filesPayload(value: unknown): value is { items: FilesItemView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { items?: unknown }).items);\n}\n\n/**\n * The files data channel: one direct-event subscription per managed session,\n * published as { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(): DoomHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const subscriptions = new Map<string, () => void>();\n const paths = new FileEditPaths();\n const channelSource: DoomHubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n let timelinePath: string | undefined;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // The session has no readable state path yet.\n }\n const items = timelinePath === undefined ? [] : readSessionFiles(timelinePath, scope.cwd);\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n const unsubscribe = host.directEvents.subscribe(\n filesChannelType,\n scope.sessionId,\n (payload) => {\n if (!filesPayload(payload)) return;\n latest.set(scope.sessionId, payload.items);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n type DoomHeadlessExecutionContext,\n type DoomHeadlessActivity,\n type DoomHeadlessHook,\n} from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerPluginContext, DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createDoomIgnoreMatcher } from '../../services/doomIgnore';\nimport { EditTracker } from '../../services/editTracker';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { NodeGitStatusAdapter } from '../../services/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport { NodeTreeManifestAdapter } from '../../services/treeManifest';\nimport { filesChannelType, filesStatusKey } from '../../types/webFiles';\nimport { api } from '../fileEditsApi';\nimport { readSessionFiles } from '../webFilesChannel';\n\nconst DOOM_IGNORE_FILE = '.doomignore';\n\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n try {\n const matcher = createDoomIgnoreMatcher(fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8'));\n return matcher === undefined ? undefined : (filePath: string) => matcher(path.relative(cwd, filePath));\n } catch {\n return undefined;\n }\n}\n\nfunction stringValue(value: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const candidate = value[key];\n return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;\n}\n\nexport function createFileEditSession({ agent, host }: DoomServerPluginContext): DoomServerSessionPlugin {\n if (!agent) return { api: [api] };\n const paths = new FileEditPaths();\n const timeline = new TimelineStore();\n const snapshots = new NodeSnapshotStoreAdapter();\n const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), {\n git: new NodeGitStatusAdapter(),\n });\n if (host.context.directEvents === undefined)\n throw new Error('File-edit headless facet requires the session direct event bus.');\n const directEvents = host.context.directEvents;\n let active:\n | {\n readonly context: DoomHeadlessExecutionContext;\n readonly timelinePath: string;\n readonly snapshotsPath: string;\n }\n | undefined;\n /**\n * The channel carries the rows; the status decides whether the group exists.\n *\n * A cockpit drives this facet and never the Pi runtime, so without the status\n * the dock had the file list on the wire and no frame to draw it in. The empty\n * string is deliberate: the group declares `hideWhenEmpty`, so a session that\n * has edited nothing publishes a key with no content and stays hidden.\n */\n const publish = (runtime: NonNullable<typeof active>): void => {\n const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);\n directEvents.publish(filesChannelType, runtime.context.sessionId, { items });\n runtime.context.client.setStatus(\n filesStatusKey,\n items.length === 0 ? '' : `${items.length} ${items.length === 1 ? 'file' : 'files'}`,\n );\n };\n const activity: DoomHeadlessActivity = {\n name: 'file-edits',\n start: async (executionContext) => {\n const sessionKey = paths.sessionKey(executionContext.sessionId);\n const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n const isIgnored = readDoomIgnore(executionContext.cwd);\n editTracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n const runtime = { context: executionContext, timelinePath, snapshotsPath };\n active = runtime;\n publish(runtime);\n return async () => {\n if (active !== runtime) return;\n active = undefined;\n executionContext.client.setStatus(filesStatusKey, undefined);\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_execution_start',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n const toolName = stringValue(event, 'toolName');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId || !toolName)\n return;\n await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);\n },\n };\n const endHook: DoomHeadlessHook = {\n event: 'tool_execution_end',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId) return;\n await editTracker.end(toolCallId, event.isError === true, executionContext.cwd);\n if (active === runtime) publish(runtime);\n },\n };\n return { api: [api], activities: [activity], hooks: [startHook, endHook] };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createFileEditSession } from '../../../../../services/fileEditSession';\nexport const createFileEditServerRoot = (context: DoomServerPluginContext) => {\n const runtime = createFileEditSession(context);\n return { value: runtime, activities: runtime.activities };\n};\nexport type FileEditServerScope = Awaited<ReturnType<typeof createFileEditServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createFileEditServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createFileEditServerRoot);\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createFilesChannel } from '../../../services/webFilesChannel';\nexport default defineChannel(function fileEditsChannel(): DoomHubChannel {\n return createFilesChannel();\n});\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/fileEditsApi';\n\n/**\n * Mounts this package's HTTP API at `file-edits`, which the folder names.\n *\n * Until this file existed the API was never mounted at all. The session root\n * returns a runtime carrying `api`, but a root's `api` is not folded into the\n * generated entry the way its services and activities are, so the registration\n * was dropped in silence and the cockpit called routes that answered 404.\n */\nexport default defineRoute(api);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![1]!);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![0]!);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelFileEdits from '../src/extensions/(backend)/channel/fileEdits.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server';\nimport hookToolExecutionEnd from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server';\nimport hookToolExecutionStart from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-file-edit',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n get hooks(): DoomServerSessionPlugin['hooks'] { return [via({ event: 'tool_execution_end' }, at(hookToolExecutionEnd, contextSession)), via({ event: 'tool_execution_start' }, at(hookToolExecutionStart, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../generated/server.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../../generated/server.ts"],"mappings":";qBAgBa,oDAAK"}
|
|
@@ -599,10 +599,10 @@ var tool_execution_start_server_default = defineHook((context) => context.root.h
|
|
|
599
599
|
//#endregion
|
|
600
600
|
//#region generated/server.ts
|
|
601
601
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
602
|
-
const via = (identity, value) =>
|
|
602
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
603
603
|
...identity,
|
|
604
604
|
...value
|
|
605
|
-
}
|
|
605
|
+
};
|
|
606
606
|
const facet = defineServerPlugin({
|
|
607
607
|
name: "@agimon-ai/doompi-file-edit",
|
|
608
608
|
global: () => ({ get channels() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":["routes","fs","fs","channelFileEdits","rootSession","apiRoute","hookToolExecutionEnd","hookToolExecutionStart"],"sources":["../../src/types/apiRoutes.ts","../../src/types/fileEditsApi.ts","../../src/services/fileEditsApi/index.ts","../../src/services/webFilesChannel/index.ts","../../src/services/fileEditSession/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/extensions/(backend)/channel/fileEdits.server.ts","../../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts","../../generated/server.ts"],"sourcesContent":["import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\n\nimport { baselineOf } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { lineDiff, lineDiffFromEmpty } from '../../services/lineDiff';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport routes from '../../types/apiRoutes';\nimport type { FileEditVersion } from '../../types/domain';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsPreviewView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../../types/fileEditsApi';\nimport type { SnapshotStorePort } from '../../types/snapshotStore';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\n/**\n * This package's HTTP surface: one file's history, and the manual save.\n *\n * Routes are relative to the mount its host gives it, so nothing here repeats\n * where it was mounted. The host is one session's own server, which is what\n * makes the session id and working directory available without the page\n * supplying either: a page names a file, never a session's private paths.\n *\n * The detail route answers everything a reader needs to open a changed file,\n * because splitting it would make the page stitch three responses together for\n * one click. The save route is the only mutation, and it refuses unless the\n * reader proves they were looking at the content that is still on disk. The\n * preview route is the read-only way in to a file the session never changed,\n * bounded by the working directory instead of by the timeline.\n */\n\n/** Past this, the working view reports the file rather than carrying it. */\nexport const MAX_WORKING_BYTES = 1024 * 1024;\n\nconst BINARY_SAMPLE_BYTES = 8192;\nconst NO_BASELINE_NOTE = 'changed by a command, so no baseline was captured; showing the file as it stands';\n/**\n * A change the tool layer named but never captured content for. That is what\n * every record written before this package learned to snapshot looks like, and\n * calling those \"changed by a command\" would be wrong: the tool is right there\n * in the record.\n */\nconst NO_SNAPSHOT_NOTE = 'recorded before this session began capturing content, so there is no diff for it';\nconst NO_AFTER_NOTE = 'the new content could not be captured: the file is binary or past the size cap';\nconst NO_BEFORE_NOTE = 'the previous content could not be captured: it was binary or past the size cap';\nconst APPROXIMATE_NOTE = 'the changed region was too large to match line by line';\n\nexport interface FileEditsApiOptions {\n /** The session these routes answer for; absent when the host is the hub. */\n sessionId?: string;\n /** The session's working directory, which relative paths are shown against. */\n cwd?: string;\n timeline?: ITimelineStore;\n snapshots?: SnapshotStorePort;\n paths?: IFileEditPaths;\n}\n\nfunction hashOf(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Reads the file as the source view will show it, or says why it cannot. */\nasync function readWorking(filePath: string): Promise<FileEditsWorkingView> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch {\n return { content: '', hash: '', unavailable: true, reason: 'the file no longer exists' };\n }\n if (raw.byteLength > MAX_WORKING_BYTES) {\n return { content: '', hash: '', unavailable: true, reason: `the file is larger than ${MAX_WORKING_BYTES} bytes` };\n }\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n return { content: '', hash: '', unavailable: true, reason: 'the file is binary' };\n }\n const content = raw.toString('utf8');\n return { content, hash: hashOf(content), unavailable: false };\n}\n\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * The same containment check after symlinks are followed. A path that does not\n * exist yet resolves to nothing, and there is nothing to hand out, so it\n * passes and the read below reports it missing.\n */\nasync function isRealPathInside(root: string, candidate: string): Promise<boolean> {\n let realRoot: string;\n let real: string;\n try {\n realRoot = await fs.realpath(root);\n real = await fs.realpath(candidate);\n } catch {\n return true;\n }\n return isInside(realRoot, real);\n}\n\n/**\n * One recorded change as the page renders it. A change with both sides captured\n * carries its own diff; one without says why, rather than showing nothing.\n *\n * A file this session created counts as captured on both sides. Its baseline is\n * empty rather than missing, so the whole file reads as arriving at once, which\n * is exactly what happened.\n */\nasync function toVersionView(version: FileEditVersion, snapshots: SnapshotStorePort): Promise<FileEditsVersionView> {\n const view: FileEditsVersionView = {\n index: version.index,\n tool: version.tool,\n at: version.at,\n origin: version.origin,\n additions: version.additions ?? 0,\n removals: version.removals ?? 0,\n };\n const createdHere = version.before === undefined && version.created === true;\n if (version.after === undefined || (version.before === undefined && !createdHere)) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but content was not stored: either it did not fit, or\n // the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: version.after === undefined ? NO_AFTER_NOTE : NO_BEFORE_NOTE };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const after = await snapshots.read(version.after);\n if (after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n if (version.before === undefined) {\n const fresh = lineDiffFromEmpty(after);\n return {\n ...view,\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(version.before);\n if (before === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n const diff = lineDiff(before, after);\n return {\n ...view,\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\n/** Everything the session did to the file: its oldest baseline against what is on disk now. */\nasync function toCumulativeView(\n versions: readonly FileEditVersion[],\n working: FileEditsWorkingView,\n snapshots: SnapshotStorePort,\n): Promise<FileEditsCumulativeView> {\n // A file this session created starts from nothing, and nothing is a baseline\n // the snapshot store never has to hold.\n const createdHere = versions[0]?.created === true;\n const baseline = baselineOf(versions);\n if (!createdHere && baseline === undefined) {\n return {\n additions: 0,\n removals: 0,\n note: versions[0]?.origin === 'tool' ? NO_BEFORE_NOTE : NO_BASELINE_NOTE,\n };\n }\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n if (createdHere || baseline === undefined) {\n const fresh = lineDiffFromEmpty(working.content);\n return {\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(baseline);\n if (before === undefined) {\n return { additions: 0, removals: 0, note: 'the content this session started from is no longer held' };\n }\n const diff = lineDiff(before, working.content);\n return {\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\nexport function createFileEditsApi(options: FileEditsApiOptions = {}): Hono {\n const timeline = options.timeline ?? new TimelineStore();\n const snapshots = options.snapshots ?? new NodeSnapshotStoreAdapter();\n const paths = options.paths ?? new FileEditPaths();\n const cwd = options.cwd ?? '';\n const app = new Hono();\n\n // The routes and the extension run in different processes, so this half\n // opens the same two locations from the same inputs rather than sharing an\n // instance with the half that writes them.\n //\n // Only what this function built gets pointed at them. A store handed in\n // through the options arrives already configured, and repointing it would\n // make the option a lie: the caller would be injecting a store the routes\n // then read somewhere else entirely.\n if (options.sessionId !== undefined && cwd !== '') {\n const sessionKey = paths.sessionKey(options.sessionId);\n if (options.timeline === undefined) timeline.initialize(paths.timelinePath(cwd, sessionKey));\n if (options.snapshots === undefined) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));\n }\n\n /**\n * The file a request names, rejected unless this session actually recorded a\n * change to it. That check is the authorization: these routes read and write\n * whatever path they are handed, so the timeline is what bounds them to files\n * the session already touched.\n */\n const recordedVersions = async (filePath: string | undefined): Promise<FileEditVersion[] | undefined> => {\n if (filePath === undefined || filePath === '') return undefined;\n const versions = await timeline.versions(path.resolve(cwd, filePath));\n return versions.length === 0 ? undefined : versions;\n };\n\n app.get(routes.detail.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n const versions = await recordedVersions(requested);\n if (versions === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n const working = await readWorking(filePath);\n const body: FileEditsDetailView = {\n path: filePath,\n relPath: cwd === '' ? filePath : path.relative(cwd, filePath),\n versions: await Promise.all(versions.map((version) => toVersionView(version, snapshots))),\n cumulative: await toCumulativeView(versions, working, snapshots),\n working,\n };\n return context.json(body);\n });\n\n /**\n * The file a request names, read only, for a file the timeline knows nothing\n * about: a path the agent read but never changed is still a path the reader\n * clicked, and refusing it would leave the link dead.\n *\n * The working directory is the boundary here, since the timeline is not.\n * Containment is checked lexically and again on the real path, so a symlink\n * inside the tree cannot hand out something beyond it, and a host that gave\n * these routes no working directory has no boundary to enforce and so serves\n * nothing.\n */\n app.get(routes.preview.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if (requested === undefined || requested === '' || requested.includes('\\0')) {\n return context.json({ error: 'A preview names a path.' }, 400);\n }\n if (cwd === '') return context.json({ error: 'This session has no working directory to read from.' }, 403);\n const root = path.resolve(cwd);\n const filePath = path.resolve(root, requested);\n if (!isInside(root, filePath) || !(await isRealPathInside(root, filePath))) {\n return context.json({ error: 'The path leaves the session directory.' }, 403);\n }\n const body: FileEditsPreviewView = {\n path: filePath,\n relPath: path.relative(root, filePath),\n working: await readWorking(filePath),\n };\n return context.json(body);\n });\n\n app.put(routes.save.path, async (context) => {\n let request: FileEditsSaveRequest;\n try {\n request = (await context.req.json()) as FileEditsSaveRequest;\n } catch {\n return context.json({ error: 'The save body is not JSON.' }, 400);\n }\n if (typeof request.path !== 'string' || typeof request.content !== 'string') {\n return context.json({ error: 'A save names a path and the content to write.' }, 400);\n }\n if ((await recordedVersions(request.path)) === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, request.path);\n const current = await readWorking(filePath);\n if (current.unavailable) {\n return context.json({ error: current.reason ?? 'The file cannot be read.' }, 409);\n }\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. Refusing on a moved hash is what\n // stops the save from silently discarding whatever it did.\n if (current.hash !== request.expectedHash) {\n return context.json({ error: 'The file changed since it was opened.', hash: current.hash }, 409);\n }\n await fs.writeFile(filePath, request.content, 'utf8');\n const body: FileEditsSaveView = { hash: hashOf(request.content) };\n return context.json(body);\n });\n\n app.delete(routes.remove.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if ((await recordedVersions(requested)) === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n try {\n await fs.unlink(filePath);\n } catch (error) {\n // Already gone is the outcome the caller wanted, so it is not a failure.\n if (!hasCode(error, 'ENOENT')) {\n return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);\n }\n }\n return context.body(null, 204);\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built entry. */\nexport const api: DoomApi = {\n basePath: API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n const app = createFileEditsApi({\n ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),\n ...(context.cwd === undefined ? {} : { cwd: context.cwd }),\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request here: every route reads, answers and closes.\n close: () => undefined,\n };\n },\n};\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { filterDoomIgnoredFiles } from '../../services/doomIgnore';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { filesChannelType, type FilesItemView } from '../../types/webFiles';\n\n/**\n * The changed-files channel consumes snapshots emitted by the owning session\n * facet. Durable timeline reads seed a channel that starts after session startup.\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\nfunction filesPayload(value: unknown): value is { items: FilesItemView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { items?: unknown }).items);\n}\n\n/**\n * The files data channel: one direct-event subscription per managed session,\n * published as { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(): DoomHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const subscriptions = new Map<string, () => void>();\n const paths = new FileEditPaths();\n const channelSource: DoomHubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n let timelinePath: string | undefined;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // The session has no readable state path yet.\n }\n const items = timelinePath === undefined ? [] : readSessionFiles(timelinePath, scope.cwd);\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n const unsubscribe = host.directEvents.subscribe(\n filesChannelType,\n scope.sessionId,\n (payload) => {\n if (!filesPayload(payload)) return;\n latest.set(scope.sessionId, payload.items);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n type DoomHeadlessExecutionContext,\n type DoomHeadlessActivity,\n type DoomHeadlessHook,\n} from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerPluginContext, DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createDoomIgnoreMatcher } from '../../services/doomIgnore';\nimport { EditTracker } from '../../services/editTracker';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { NodeGitStatusAdapter } from '../../services/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport { NodeTreeManifestAdapter } from '../../services/treeManifest';\nimport { filesChannelType, filesStatusKey } from '../../types/webFiles';\nimport { api } from '../fileEditsApi';\nimport { readSessionFiles } from '../webFilesChannel';\n\nconst DOOM_IGNORE_FILE = '.doomignore';\n\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n try {\n const matcher = createDoomIgnoreMatcher(fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8'));\n return matcher === undefined ? undefined : (filePath: string) => matcher(path.relative(cwd, filePath));\n } catch {\n return undefined;\n }\n}\n\nfunction stringValue(value: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const candidate = value[key];\n return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;\n}\n\nexport function createFileEditSession({ agent, host }: DoomServerPluginContext): DoomServerSessionPlugin {\n if (!agent) return { api: [api] };\n const paths = new FileEditPaths();\n const timeline = new TimelineStore();\n const snapshots = new NodeSnapshotStoreAdapter();\n const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), {\n git: new NodeGitStatusAdapter(),\n });\n if (host.context.directEvents === undefined)\n throw new Error('File-edit headless facet requires the session direct event bus.');\n const directEvents = host.context.directEvents;\n let active:\n | {\n readonly context: DoomHeadlessExecutionContext;\n readonly timelinePath: string;\n readonly snapshotsPath: string;\n }\n | undefined;\n /**\n * The channel carries the rows; the status decides whether the group exists.\n *\n * A cockpit drives this facet and never the Pi runtime, so without the status\n * the dock had the file list on the wire and no frame to draw it in. The empty\n * string is deliberate: the group declares `hideWhenEmpty`, so a session that\n * has edited nothing publishes a key with no content and stays hidden.\n */\n const publish = (runtime: NonNullable<typeof active>): void => {\n const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);\n directEvents.publish(filesChannelType, runtime.context.sessionId, { items });\n runtime.context.client.setStatus(\n filesStatusKey,\n items.length === 0 ? '' : `${items.length} ${items.length === 1 ? 'file' : 'files'}`,\n );\n };\n const activity: DoomHeadlessActivity = {\n name: 'file-edits',\n start: async (executionContext) => {\n const sessionKey = paths.sessionKey(executionContext.sessionId);\n const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n const isIgnored = readDoomIgnore(executionContext.cwd);\n editTracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n const runtime = { context: executionContext, timelinePath, snapshotsPath };\n active = runtime;\n publish(runtime);\n return async () => {\n if (active !== runtime) return;\n active = undefined;\n executionContext.client.setStatus(filesStatusKey, undefined);\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_execution_start',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n const toolName = stringValue(event, 'toolName');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId || !toolName)\n return;\n await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);\n },\n };\n const endHook: DoomHeadlessHook = {\n event: 'tool_execution_end',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId) return;\n await editTracker.end(toolCallId, event.isError === true, executionContext.cwd);\n if (active === runtime) publish(runtime);\n },\n };\n return { api: [api], activities: [activity], hooks: [startHook, endHook] };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createFileEditSession } from '../../../../../services/fileEditSession';\nexport const createFileEditServerRoot = (context: DoomServerPluginContext) => {\n const runtime = createFileEditSession(context);\n return { value: runtime, activities: runtime.activities };\n};\nexport type FileEditServerScope = Awaited<ReturnType<typeof createFileEditServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createFileEditServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createFileEditServerRoot);\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createFilesChannel } from '../../../services/webFilesChannel';\nexport default defineChannel(function fileEditsChannel(): DoomHubChannel {\n return createFilesChannel();\n});\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/fileEditsApi';\n\n/**\n * Mounts this package's HTTP API at `file-edits`, which the folder names.\n *\n * Until this file existed the API was never mounted at all. The session root\n * returns a runtime carrying `api`, but a root's `api` is not folded into the\n * generated entry the way its services and activities are, so the registration\n * was dropped in silence and the cockpit called routes that answered 404.\n */\nexport default defineRoute(api);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![1]!);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![0]!);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelFileEdits from '../src/extensions/(backend)/channel/fileEdits.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server';\nimport hookToolExecutionEnd from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server';\nimport hookToolExecutionStart from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T => ({ ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-file-edit',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n get hooks(): DoomServerSessionPlugin['hooks'] { return [via({ event: 'tool_execution_end' }, at(hookToolExecutionEnd, contextSession)), via({ event: 'tool_execution_start' }, at(hookToolExecutionStart, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":["routes","fs","fs","channelFileEdits","rootSession","apiRoute","hookToolExecutionEnd","hookToolExecutionStart"],"sources":["../../src/types/apiRoutes.ts","../../src/types/fileEditsApi.ts","../../src/services/fileEditsApi/index.ts","../../src/services/webFilesChannel/index.ts","../../src/services/fileEditSession/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/extensions/(backend)/channel/fileEdits.server.ts","../../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts","../../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts","../../generated/server.ts"],"sourcesContent":["import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","import { createHash } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\n\nimport { baselineOf } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { lineDiff, lineDiffFromEmpty } from '../../services/lineDiff';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport routes from '../../types/apiRoutes';\nimport type { FileEditVersion } from '../../types/domain';\nimport type { IFileEditPaths } from '../../types/fileEditPaths';\nimport {\n API_BASE_PATH,\n type FileEditsCumulativeView,\n type FileEditsDetailView,\n type FileEditsPreviewView,\n type FileEditsSaveRequest,\n type FileEditsSaveView,\n type FileEditsVersionView,\n type FileEditsWorkingView,\n PATH_QUERY_PARAM,\n} from '../../types/fileEditsApi';\nimport type { SnapshotStorePort } from '../../types/snapshotStore';\nimport type { ITimelineStore } from '../../types/timelineStore';\n\n/**\n * This package's HTTP surface: one file's history, and the manual save.\n *\n * Routes are relative to the mount its host gives it, so nothing here repeats\n * where it was mounted. The host is one session's own server, which is what\n * makes the session id and working directory available without the page\n * supplying either: a page names a file, never a session's private paths.\n *\n * The detail route answers everything a reader needs to open a changed file,\n * because splitting it would make the page stitch three responses together for\n * one click. The save route is the only mutation, and it refuses unless the\n * reader proves they were looking at the content that is still on disk. The\n * preview route is the read-only way in to a file the session never changed,\n * bounded by the working directory instead of by the timeline.\n */\n\n/** Past this, the working view reports the file rather than carrying it. */\nexport const MAX_WORKING_BYTES = 1024 * 1024;\n\nconst BINARY_SAMPLE_BYTES = 8192;\nconst NO_BASELINE_NOTE = 'changed by a command, so no baseline was captured; showing the file as it stands';\n/**\n * A change the tool layer named but never captured content for. That is what\n * every record written before this package learned to snapshot looks like, and\n * calling those \"changed by a command\" would be wrong: the tool is right there\n * in the record.\n */\nconst NO_SNAPSHOT_NOTE = 'recorded before this session began capturing content, so there is no diff for it';\nconst NO_AFTER_NOTE = 'the new content could not be captured: the file is binary or past the size cap';\nconst NO_BEFORE_NOTE = 'the previous content could not be captured: it was binary or past the size cap';\nconst APPROXIMATE_NOTE = 'the changed region was too large to match line by line';\n\nexport interface FileEditsApiOptions {\n /** The session these routes answer for; absent when the host is the hub. */\n sessionId?: string;\n /** The session's working directory, which relative paths are shown against. */\n cwd?: string;\n timeline?: ITimelineStore;\n snapshots?: SnapshotStorePort;\n paths?: IFileEditPaths;\n}\n\nfunction hashOf(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return error instanceof Error && 'code' in error && error.code === code;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Reads the file as the source view will show it, or says why it cannot. */\nasync function readWorking(filePath: string): Promise<FileEditsWorkingView> {\n let raw: Buffer;\n try {\n raw = await fs.readFile(filePath);\n } catch {\n return { content: '', hash: '', unavailable: true, reason: 'the file no longer exists' };\n }\n if (raw.byteLength > MAX_WORKING_BYTES) {\n return { content: '', hash: '', unavailable: true, reason: `the file is larger than ${MAX_WORKING_BYTES} bytes` };\n }\n if (raw.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n return { content: '', hash: '', unavailable: true, reason: 'the file is binary' };\n }\n const content = raw.toString('utf8');\n return { content, hash: hashOf(content), unavailable: false };\n}\n\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * The same containment check after symlinks are followed. A path that does not\n * exist yet resolves to nothing, and there is nothing to hand out, so it\n * passes and the read below reports it missing.\n */\nasync function isRealPathInside(root: string, candidate: string): Promise<boolean> {\n let realRoot: string;\n let real: string;\n try {\n realRoot = await fs.realpath(root);\n real = await fs.realpath(candidate);\n } catch {\n return true;\n }\n return isInside(realRoot, real);\n}\n\n/**\n * One recorded change as the page renders it. A change with both sides captured\n * carries its own diff; one without says why, rather than showing nothing.\n *\n * A file this session created counts as captured on both sides. Its baseline is\n * empty rather than missing, so the whole file reads as arriving at once, which\n * is exactly what happened.\n */\nasync function toVersionView(version: FileEditVersion, snapshots: SnapshotStorePort): Promise<FileEditsVersionView> {\n const view: FileEditsVersionView = {\n index: version.index,\n tool: version.tool,\n at: version.at,\n origin: version.origin,\n additions: version.additions ?? 0,\n removals: version.removals ?? 0,\n };\n const createdHere = version.before === undefined && version.created === true;\n if (version.after === undefined || (version.before === undefined && !createdHere)) {\n // A scan found the path only after it changed, so no baseline exists. A\n // tool named the path but content was not stored: either it did not fit, or\n // the record predates content capture, which the tool tells apart.\n if (version.origin === 'tool') {\n return { ...view, note: version.after === undefined ? NO_AFTER_NOTE : NO_BEFORE_NOTE };\n }\n return { ...view, note: version.tool === 'bash' ? NO_BASELINE_NOTE : NO_SNAPSHOT_NOTE };\n }\n const after = await snapshots.read(version.after);\n if (after === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n if (version.before === undefined) {\n const fresh = lineDiffFromEmpty(after);\n return {\n ...view,\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(version.before);\n if (before === undefined) {\n return { ...view, note: 'the content behind this change is no longer held' };\n }\n const diff = lineDiff(before, after);\n return {\n ...view,\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\n/** Everything the session did to the file: its oldest baseline against what is on disk now. */\nasync function toCumulativeView(\n versions: readonly FileEditVersion[],\n working: FileEditsWorkingView,\n snapshots: SnapshotStorePort,\n): Promise<FileEditsCumulativeView> {\n // A file this session created starts from nothing, and nothing is a baseline\n // the snapshot store never has to hold.\n const createdHere = versions[0]?.created === true;\n const baseline = baselineOf(versions);\n if (!createdHere && baseline === undefined) {\n return {\n additions: 0,\n removals: 0,\n note: versions[0]?.origin === 'tool' ? NO_BEFORE_NOTE : NO_BASELINE_NOTE,\n };\n }\n if (working.unavailable) {\n return { additions: 0, removals: 0, note: working.reason ?? 'the file cannot be read' };\n }\n if (createdHere || baseline === undefined) {\n const fresh = lineDiffFromEmpty(working.content);\n return {\n additions: fresh.additions,\n removals: fresh.removals,\n hunks: fresh.hunks,\n ...(fresh.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n }\n const before = await snapshots.read(baseline);\n if (before === undefined) {\n return { additions: 0, removals: 0, note: 'the content this session started from is no longer held' };\n }\n const diff = lineDiff(before, working.content);\n return {\n additions: diff.additions,\n removals: diff.removals,\n hunks: diff.hunks,\n ...(diff.approximate ? { note: APPROXIMATE_NOTE } : {}),\n };\n}\n\nexport function createFileEditsApi(options: FileEditsApiOptions = {}): Hono {\n const timeline = options.timeline ?? new TimelineStore();\n const snapshots = options.snapshots ?? new NodeSnapshotStoreAdapter();\n const paths = options.paths ?? new FileEditPaths();\n const cwd = options.cwd ?? '';\n const app = new Hono();\n\n // The routes and the extension run in different processes, so this half\n // opens the same two locations from the same inputs rather than sharing an\n // instance with the half that writes them.\n //\n // Only what this function built gets pointed at them. A store handed in\n // through the options arrives already configured, and repointing it would\n // make the option a lie: the caller would be injecting a store the routes\n // then read somewhere else entirely.\n if (options.sessionId !== undefined && cwd !== '') {\n const sessionKey = paths.sessionKey(options.sessionId);\n if (options.timeline === undefined) timeline.initialize(paths.timelinePath(cwd, sessionKey));\n if (options.snapshots === undefined) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));\n }\n\n /**\n * The file a request names, rejected unless this session actually recorded a\n * change to it. That check is the authorization: these routes read and write\n * whatever path they are handed, so the timeline is what bounds them to files\n * the session already touched.\n */\n const recordedVersions = async (filePath: string | undefined): Promise<FileEditVersion[] | undefined> => {\n if (filePath === undefined || filePath === '') return undefined;\n const versions = await timeline.versions(path.resolve(cwd, filePath));\n return versions.length === 0 ? undefined : versions;\n };\n\n app.get(routes.detail.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n const versions = await recordedVersions(requested);\n if (versions === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n const working = await readWorking(filePath);\n const body: FileEditsDetailView = {\n path: filePath,\n relPath: cwd === '' ? filePath : path.relative(cwd, filePath),\n versions: await Promise.all(versions.map((version) => toVersionView(version, snapshots))),\n cumulative: await toCumulativeView(versions, working, snapshots),\n working,\n };\n return context.json(body);\n });\n\n /**\n * The file a request names, read only, for a file the timeline knows nothing\n * about: a path the agent read but never changed is still a path the reader\n * clicked, and refusing it would leave the link dead.\n *\n * The working directory is the boundary here, since the timeline is not.\n * Containment is checked lexically and again on the real path, so a symlink\n * inside the tree cannot hand out something beyond it, and a host that gave\n * these routes no working directory has no boundary to enforce and so serves\n * nothing.\n */\n app.get(routes.preview.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if (requested === undefined || requested === '' || requested.includes('\\0')) {\n return context.json({ error: 'A preview names a path.' }, 400);\n }\n if (cwd === '') return context.json({ error: 'This session has no working directory to read from.' }, 403);\n const root = path.resolve(cwd);\n const filePath = path.resolve(root, requested);\n if (!isInside(root, filePath) || !(await isRealPathInside(root, filePath))) {\n return context.json({ error: 'The path leaves the session directory.' }, 403);\n }\n const body: FileEditsPreviewView = {\n path: filePath,\n relPath: path.relative(root, filePath),\n working: await readWorking(filePath),\n };\n return context.json(body);\n });\n\n app.put(routes.save.path, async (context) => {\n let request: FileEditsSaveRequest;\n try {\n request = (await context.req.json()) as FileEditsSaveRequest;\n } catch {\n return context.json({ error: 'The save body is not JSON.' }, 400);\n }\n if (typeof request.path !== 'string' || typeof request.content !== 'string') {\n return context.json({ error: 'A save names a path and the content to write.' }, 400);\n }\n if ((await recordedVersions(request.path)) === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, request.path);\n const current = await readWorking(filePath);\n if (current.unavailable) {\n return context.json({ error: current.reason ?? 'The file cannot be read.' }, 409);\n }\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. Refusing on a moved hash is what\n // stops the save from silently discarding whatever it did.\n if (current.hash !== request.expectedHash) {\n return context.json({ error: 'The file changed since it was opened.', hash: current.hash }, 409);\n }\n await fs.writeFile(filePath, request.content, 'utf8');\n const body: FileEditsSaveView = { hash: hashOf(request.content) };\n return context.json(body);\n });\n\n app.delete(routes.remove.path, async (context) => {\n const requested = context.req.query(PATH_QUERY_PARAM);\n if ((await recordedVersions(requested)) === undefined || requested === undefined) {\n return context.json({ error: 'This session recorded no changes to that file.' }, 404);\n }\n const filePath = path.resolve(cwd, requested);\n try {\n await fs.unlink(filePath);\n } catch (error) {\n // Already gone is the outcome the caller wanted, so it is not a failure.\n if (!hasCode(error, 'ENOENT')) {\n return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);\n }\n }\n return context.body(null, 204);\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built entry. */\nexport const api: DoomApi = {\n basePath: API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n const app = createFileEditsApi({\n ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),\n ...(context.cwd === undefined ? {} : { cwd: context.cwd }),\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request here: every route reads, answers and closes.\n close: () => undefined,\n };\n },\n};\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { filterDoomIgnoredFiles } from '../../services/doomIgnore';\nimport { confirmedChanges, foldEntries, foldVersions, isDiffable, parseTimeline } from '../../services/fileChanges';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { filesChannelType, type FilesItemView } from '../../types/webFiles';\n\n/**\n * The changed-files channel consumes snapshots emitted by the owning session\n * facet. Durable timeline reads seed a channel that starts after session startup.\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\nfunction filesPayload(value: unknown): value is { items: FilesItemView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { items?: unknown }).items);\n}\n\n/**\n * The files data channel: one direct-event subscription per managed session,\n * published as { items } payloads under the 'file_edits' frame type.\n */\nexport function createFilesChannel(): DoomHubChannel {\n return {\n frameType: filesChannelType,\n start(host) {\n const latest = new Map<string, FilesItemView[]>();\n const subscriptions = new Map<string, () => void>();\n const paths = new FileEditPaths();\n const channelSource: DoomHubChannelSource = {\n payloadFor(scope) {\n const items = latest.get(scope.sessionId);\n return items === undefined ? undefined : { items };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n let timelinePath: string | undefined;\n try {\n timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));\n } catch {\n // The session has no readable state path yet.\n }\n const items = timelinePath === undefined ? [] : readSessionFiles(timelinePath, scope.cwd);\n latest.set(scope.sessionId, items);\n host.publish(scope.sessionId, { items });\n const unsubscribe = host.directEvents.subscribe(\n filesChannelType,\n scope.sessionId,\n (payload) => {\n if (!filesPayload(payload)) return;\n latest.set(scope.sessionId, payload.items);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return channelSource;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n type DoomHeadlessExecutionContext,\n type DoomHeadlessActivity,\n type DoomHeadlessHook,\n} from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerPluginContext, DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createDoomIgnoreMatcher } from '../../services/doomIgnore';\nimport { EditTracker } from '../../services/editTracker';\nimport { FileEditPaths } from '../../services/fileEditPaths';\nimport { NodeGitStatusAdapter } from '../../services/gitStatus';\nimport { NodeSnapshotStoreAdapter } from '../../services/snapshotStore';\nimport { TimelineStore } from '../../services/timelineStore';\nimport { NodeTreeManifestAdapter } from '../../services/treeManifest';\nimport { filesChannelType, filesStatusKey } from '../../types/webFiles';\nimport { api } from '../fileEditsApi';\nimport { readSessionFiles } from '../webFilesChannel';\n\nconst DOOM_IGNORE_FILE = '.doomignore';\n\nfunction readDoomIgnore(cwd: string): ((filePath: string) => boolean) | undefined {\n try {\n const matcher = createDoomIgnoreMatcher(fs.readFileSync(path.join(cwd, DOOM_IGNORE_FILE), 'utf8'));\n return matcher === undefined ? undefined : (filePath: string) => matcher(path.relative(cwd, filePath));\n } catch {\n return undefined;\n }\n}\n\nfunction stringValue(value: Readonly<Record<string, unknown>>, key: string): string | undefined {\n const candidate = value[key];\n return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;\n}\n\nexport function createFileEditSession({ agent, host }: DoomServerPluginContext): DoomServerSessionPlugin {\n if (!agent) return { api: [api] };\n const paths = new FileEditPaths();\n const timeline = new TimelineStore();\n const snapshots = new NodeSnapshotStoreAdapter();\n const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), {\n git: new NodeGitStatusAdapter(),\n });\n if (host.context.directEvents === undefined)\n throw new Error('File-edit headless facet requires the session direct event bus.');\n const directEvents = host.context.directEvents;\n let active:\n | {\n readonly context: DoomHeadlessExecutionContext;\n readonly timelinePath: string;\n readonly snapshotsPath: string;\n }\n | undefined;\n /**\n * The channel carries the rows; the status decides whether the group exists.\n *\n * A cockpit drives this facet and never the Pi runtime, so without the status\n * the dock had the file list on the wire and no frame to draw it in. The empty\n * string is deliberate: the group declares `hideWhenEmpty`, so a session that\n * has edited nothing publishes a key with no content and stays hidden.\n */\n const publish = (runtime: NonNullable<typeof active>): void => {\n const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);\n directEvents.publish(filesChannelType, runtime.context.sessionId, { items });\n runtime.context.client.setStatus(\n filesStatusKey,\n items.length === 0 ? '' : `${items.length} ${items.length === 1 ? 'file' : 'files'}`,\n );\n };\n const activity: DoomHeadlessActivity = {\n name: 'file-edits',\n start: async (executionContext) => {\n const sessionKey = paths.sessionKey(executionContext.sessionId);\n const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);\n const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);\n timeline.initialize(timelinePath);\n snapshots.initialize(snapshotsPath);\n const isIgnored = readDoomIgnore(executionContext.cwd);\n editTracker.reset({\n exclude: [timelinePath, `${timelinePath}.lock`, snapshotsPath],\n ...(isIgnored === undefined ? {} : { isIgnored }),\n });\n const runtime = { context: executionContext, timelinePath, snapshotsPath };\n active = runtime;\n publish(runtime);\n return async () => {\n if (active !== runtime) return;\n active = undefined;\n executionContext.client.setStatus(filesStatusKey, undefined);\n editTracker.reset();\n };\n },\n };\n const startHook: DoomHeadlessHook = {\n event: 'tool_execution_start',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n const toolName = stringValue(event, 'toolName');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId || !toolName)\n return;\n await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);\n },\n };\n const endHook: DoomHeadlessHook = {\n event: 'tool_execution_end',\n handle: async (event, executionContext) => {\n const runtime = active;\n const toolCallId = stringValue(event, 'toolCallId');\n if (runtime === undefined || runtime.context.sessionId !== executionContext.sessionId || !toolCallId) return;\n await editTracker.end(toolCallId, event.isError === true, executionContext.cwd);\n if (active === runtime) publish(runtime);\n },\n };\n return { api: [api], activities: [activity], hooks: [startHook, endHook] };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport { createFileEditSession } from '../../../../../services/fileEditSession';\nexport const createFileEditServerRoot = (context: DoomServerPluginContext) => {\n const runtime = createFileEditSession(context);\n return { value: runtime, activities: runtime.activities };\n};\nexport type FileEditServerScope = Awaited<ReturnType<typeof createFileEditServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createFileEditServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createFileEditServerRoot);\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createFilesChannel } from '../../../services/webFilesChannel';\nexport default defineChannel(function fileEditsChannel(): DoomHubChannel {\n return createFilesChannel();\n});\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/fileEditsApi';\n\n/**\n * Mounts this package's HTTP API at `file-edits`, which the folder names.\n *\n * Until this file existed the API was never mounted at all. The session root\n * returns a runtime carrying `api`, but a root's `api` is not folded into the\n * generated entry the way its services and activities are, so the registration\n * was dropped in silence and the cockpit called routes that answered 404.\n */\nexport default defineRoute(api);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![1]!);\n","import { defineHook, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { FileEditServerScope } from '../_lib/serverRoot';\nexport default defineHook((context: WithRoot<DoomServerPluginContext, FileEditServerScope>) => context.root.hooks![0]!);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelFileEdits from '../src/extensions/(backend)/channel/fileEdits.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/file-edits/route.server';\nimport hookToolExecutionEnd from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server';\nimport hookToolExecutionStart from '../src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-file-edit',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'file_edits' }, channelFileEdits())]; },\n get hooks(): DoomServerSessionPlugin['hooks'] { return [via({ event: 'tool_execution_end' }, at(hookToolExecutionEnd, contextSession)), via({ event: 'tool_execution_start' }, at(hookToolExecutionStart, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgBA,IAAA,oBAAe,gBAAgB;CAC7B,QAAQ;EACN,QAAQ;EACR,MAAM;EACN,OAAO,CAAC,MAAM;EACd,UAAU,YAAiC;CAC7C;CACA,SAAS;EACP,QAAQ;EACR,MAAM;EACN,OAAO,CAAC,MAAM;EACd,UAAU,YAAkC;CAC9C;CACA,MAAM;EACJ,QAAQ;EACR,MAAM;EACN,UAAU,YAA+B;CAC3C;;CAEA,QAAQ;EACN,QAAQ;EACR,MAAM;EACN,OAAO,CAAC,MAAM;CAChB;;;;;;;;CAQA,MAAM;EACJ,QAAQ;EACR,MAAM;EACN,OAAO,CAAC,MAAM;EACd,MAAM;CACR;AACF,CAAC;;;;;;;;;;;;;;;;;;;AClCD,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;ACwBhC,MAAa,oBAAoB;AAEjC,MAAM,sBAAsB;AAC5B,MAAM,mBAAmB;;;;;;;AAOzB,MAAM,mBAAmB;AACzB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AAYzB,SAAS,OAAO,SAAyB;CACvC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,SAAS,MAAM,CAAC,CAAC,OAAO,KAAK;AAClE;AAEA,SAAS,QAAQ,OAAgB,MAAuB;CACtD,OAAO,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS;AACrE;AAEA,SAAS,SAAS,OAAwB;CACxC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;;AAGA,eAAe,YAAY,UAAiD;CAC1E,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,GAAG,SAAS,QAAQ;CAClC,QAAQ;EACN,OAAO;GAAE,SAAS;GAAI,MAAM;GAAI,aAAa;GAAM,QAAQ;EAA4B;CACzF;CACA,IAAI,IAAI,aAAA,SACN,OAAO;EAAE,SAAS;EAAI,MAAM;EAAI,aAAa;EAAM,QAAQ,2BAA2B,kBAAkB;CAAQ;CAElH,IAAI,IAAI,SAAS,GAAG,mBAAmB,CAAC,CAAC,SAAS,CAAC,GACjD,OAAO;EAAE,SAAS;EAAI,MAAM;EAAI,aAAa;EAAM,QAAQ;CAAqB;CAElF,MAAM,UAAU,IAAI,SAAS,MAAM;CACnC,OAAO;EAAE;EAAS,MAAM,OAAO,OAAO;EAAG,aAAa;CAAM;AAC9D;AAEA,SAAS,SAAS,MAAc,WAA4B;CAC1D,OAAO,cAAc,QAAQ,UAAU,WAAW,GAAG,OAAO,KAAK,KAAK;AACxE;;;;;;AAOA,eAAe,iBAAiB,MAAc,WAAqC;CACjF,IAAI;CACJ,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,GAAG,SAAS,IAAI;EACjC,OAAO,MAAM,GAAG,SAAS,SAAS;CACpC,QAAQ;EACN,OAAO;CACT;CACA,OAAO,SAAS,UAAU,IAAI;AAChC;;;;;;;;;AAUA,eAAe,cAAc,SAA0B,WAA6D;CAClH,MAAM,OAA6B;EACjC,OAAO,QAAQ;EACf,MAAM,QAAQ;EACd,IAAI,QAAQ;EACZ,QAAQ,QAAQ;EAChB,WAAW,QAAQ,aAAa;EAChC,UAAU,QAAQ,YAAY;CAChC;CACA,MAAM,cAAc,QAAQ,WAAW,KAAA,KAAa,QAAQ,YAAY;CACxE,IAAI,QAAQ,UAAU,KAAA,KAAc,QAAQ,WAAW,KAAA,KAAa,CAAC,aAAc;EAIjF,IAAI,QAAQ,WAAW,QACrB,OAAO;GAAE,GAAG;GAAM,MAAM,QAAQ,UAAU,KAAA,IAAY,gBAAgB;EAAe;EAEvF,OAAO;GAAE,GAAG;GAAM,MAAM,QAAQ,SAAS,SAAS,mBAAmB;EAAiB;CACxF;CACA,MAAM,QAAQ,MAAM,UAAU,KAAK,QAAQ,KAAK;CAChD,IAAI,UAAU,KAAA,GACZ,OAAO;EAAE,GAAG;EAAM,MAAM;CAAmD;CAE7E,IAAI,QAAQ,WAAW,KAAA,GAAW;EAChC,MAAM,QAAQ,kBAAkB,KAAK;EACrC,OAAO;GACL,GAAG;GACH,WAAW,MAAM;GACjB,UAAU,MAAM;GAChB,OAAO,MAAM;GACb,GAAI,MAAM,cAAc,EAAE,MAAM,iBAAiB,IAAI,CAAC;EACxD;CACF;CACA,MAAM,SAAS,MAAM,UAAU,KAAK,QAAQ,MAAM;CAClD,IAAI,WAAW,KAAA,GACb,OAAO;EAAE,GAAG;EAAM,MAAM;CAAmD;CAE7E,MAAM,OAAO,SAAS,QAAQ,KAAK;CACnC,OAAO;EACL,GAAG;EACH,WAAW,KAAK;EAChB,UAAU,KAAK;EACf,OAAO,KAAK;EACZ,GAAI,KAAK,cAAc,EAAE,MAAM,iBAAiB,IAAI,CAAC;CACvD;AACF;;AAGA,eAAe,iBACb,UACA,SACA,WACkC;CAGlC,MAAM,cAAc,SAAS,EAAE,EAAE,YAAY;CAC7C,MAAM,WAAW,WAAW,QAAQ;CACpC,IAAI,CAAC,eAAe,aAAa,KAAA,GAC/B,OAAO;EACL,WAAW;EACX,UAAU;EACV,MAAM,SAAS,EAAE,EAAE,WAAW,SAAS,iBAAiB;CAC1D;CAEF,IAAI,QAAQ,aACV,OAAO;EAAE,WAAW;EAAG,UAAU;EAAG,MAAM,QAAQ,UAAU;CAA0B;CAExF,IAAI,eAAe,aAAa,KAAA,GAAW;EACzC,MAAM,QAAQ,kBAAkB,QAAQ,OAAO;EAC/C,OAAO;GACL,WAAW,MAAM;GACjB,UAAU,MAAM;GAChB,OAAO,MAAM;GACb,GAAI,MAAM,cAAc,EAAE,MAAM,iBAAiB,IAAI,CAAC;EACxD;CACF;CACA,MAAM,SAAS,MAAM,UAAU,KAAK,QAAQ;CAC5C,IAAI,WAAW,KAAA,GACb,OAAO;EAAE,WAAW;EAAG,UAAU;EAAG,MAAM;CAA0D;CAEtG,MAAM,OAAO,SAAS,QAAQ,QAAQ,OAAO;CAC7C,OAAO;EACL,WAAW,KAAK;EAChB,UAAU,KAAK;EACf,OAAO,KAAK;EACZ,GAAI,KAAK,cAAc,EAAE,MAAM,iBAAiB,IAAI,CAAC;CACvD;AACF;AAEA,SAAgB,mBAAmB,UAA+B,CAAC,GAAS;CAC1E,MAAM,WAAW,QAAQ,YAAY,IAAI,cAAc;CACvD,MAAM,YAAY,QAAQ,aAAa,IAAI,yBAAyB;CACpE,MAAM,QAAQ,QAAQ,SAAS,IAAI,cAAc;CACjD,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,MAAM,IAAI,KAAK;CAUrB,IAAI,QAAQ,cAAc,KAAA,KAAa,QAAQ,IAAI;EACjD,MAAM,aAAa,MAAM,WAAW,QAAQ,SAAS;EACrD,IAAI,QAAQ,aAAa,KAAA,GAAW,SAAS,WAAW,MAAM,aAAa,KAAK,UAAU,CAAC;EAC3F,IAAI,QAAQ,cAAc,KAAA,GAAW,UAAU,WAAW,MAAM,cAAc,KAAK,UAAU,CAAC;CAChG;;;;;;;CAQA,MAAM,mBAAmB,OAAO,aAAyE;EACvG,IAAI,aAAa,KAAA,KAAa,aAAa,IAAI,OAAO,KAAA;EACtD,MAAM,WAAW,MAAM,SAAS,SAAS,KAAK,QAAQ,KAAK,QAAQ,CAAC;EACpE,OAAO,SAAS,WAAW,IAAI,KAAA,IAAY;CAC7C;CAEA,IAAI,IAAIA,kBAAO,OAAO,MAAM,OAAO,YAAY;EAC7C,MAAM,YAAY,QAAQ,IAAI,MAAM,gBAAgB;EACpD,MAAM,WAAW,MAAM,iBAAiB,SAAS;EACjD,IAAI,aAAa,KAAA,KAAa,cAAc,KAAA,GAC1C,OAAO,QAAQ,KAAK,EAAE,OAAO,iDAAiD,GAAG,GAAG;EAEtF,MAAM,WAAW,KAAK,QAAQ,KAAK,SAAS;EAC5C,MAAM,UAAU,MAAM,YAAY,QAAQ;EAC1C,MAAM,OAA4B;GAChC,MAAM;GACN,SAAS,QAAQ,KAAK,WAAW,KAAK,SAAS,KAAK,QAAQ;GAC5D,UAAU,MAAM,QAAQ,IAAI,SAAS,KAAK,YAAY,cAAc,SAAS,SAAS,CAAC,CAAC;GACxF,YAAY,MAAM,iBAAiB,UAAU,SAAS,SAAS;GAC/D;EACF;EACA,OAAO,QAAQ,KAAK,IAAI;CAC1B,CAAC;;;;;;;;;;;;CAaD,IAAI,IAAIA,kBAAO,QAAQ,MAAM,OAAO,YAAY;EAC9C,MAAM,YAAY,QAAQ,IAAI,MAAM,gBAAgB;EACpD,IAAI,cAAc,KAAA,KAAa,cAAc,MAAM,UAAU,SAAS,IAAI,GACxE,OAAO,QAAQ,KAAK,EAAE,OAAO,0BAA0B,GAAG,GAAG;EAE/D,IAAI,QAAQ,IAAI,OAAO,QAAQ,KAAK,EAAE,OAAO,sDAAsD,GAAG,GAAG;EACzG,MAAM,OAAO,KAAK,QAAQ,GAAG;EAC7B,MAAM,WAAW,KAAK,QAAQ,MAAM,SAAS;EAC7C,IAAI,CAAC,SAAS,MAAM,QAAQ,KAAK,CAAE,MAAM,iBAAiB,MAAM,QAAQ,GACtE,OAAO,QAAQ,KAAK,EAAE,OAAO,yCAAyC,GAAG,GAAG;EAE9E,MAAM,OAA6B;GACjC,MAAM;GACN,SAAS,KAAK,SAAS,MAAM,QAAQ;GACrC,SAAS,MAAM,YAAY,QAAQ;EACrC;EACA,OAAO,QAAQ,KAAK,IAAI;CAC1B,CAAC;CAED,IAAI,IAAIA,kBAAO,KAAK,MAAM,OAAO,YAAY;EAC3C,IAAI;EACJ,IAAI;GACF,UAAW,MAAM,QAAQ,IAAI,KAAK;EACpC,QAAQ;GACN,OAAO,QAAQ,KAAK,EAAE,OAAO,6BAA6B,GAAG,GAAG;EAClE;EACA,IAAI,OAAO,QAAQ,SAAS,YAAY,OAAO,QAAQ,YAAY,UACjE,OAAO,QAAQ,KAAK,EAAE,OAAO,gDAAgD,GAAG,GAAG;EAErF,IAAK,MAAM,iBAAiB,QAAQ,IAAI,MAAO,KAAA,GAC7C,OAAO,QAAQ,KAAK,EAAE,OAAO,iDAAiD,GAAG,GAAG;EAEtF,MAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,IAAI;EAC/C,MAAM,UAAU,MAAM,YAAY,QAAQ;EAC1C,IAAI,QAAQ,aACV,OAAO,QAAQ,KAAK,EAAE,OAAO,QAAQ,UAAU,2BAA2B,GAAG,GAAG;EAKlF,IAAI,QAAQ,SAAS,QAAQ,cAC3B,OAAO,QAAQ,KAAK;GAAE,OAAO;GAAyC,MAAM,QAAQ;EAAK,GAAG,GAAG;EAEjG,MAAM,GAAG,UAAU,UAAU,QAAQ,SAAS,MAAM;EACpD,MAAM,OAA0B,EAAE,MAAM,OAAO,QAAQ,OAAO,EAAE;EAChE,OAAO,QAAQ,KAAK,IAAI;CAC1B,CAAC;CAED,IAAI,OAAOA,kBAAO,OAAO,MAAM,OAAO,YAAY;EAChD,MAAM,YAAY,QAAQ,IAAI,MAAM,gBAAgB;EACpD,IAAK,MAAM,iBAAiB,SAAS,MAAO,KAAA,KAAa,cAAc,KAAA,GACrE,OAAO,QAAQ,KAAK,EAAE,OAAO,iDAAiD,GAAG,GAAG;EAEtF,MAAM,WAAW,KAAK,QAAQ,KAAK,SAAS;EAC5C,IAAI;GACF,MAAM,GAAG,OAAO,QAAQ;EAC1B,SAAS,OAAO;GAEd,IAAI,CAAC,QAAQ,OAAO,QAAQ,GAC1B,OAAO,QAAQ,KAAK,EAAE,OAAO,kCAAkC,SAAS,KAAK,IAAI,GAAG,GAAG;EAE3F;EACA,OAAO,QAAQ,KAAK,MAAM,GAAG;CAC/B,CAAC;CAED,OAAO;AACT;;AAGA,MAAa,MAAe;CAC1B,UAAU;CACV,MAAM,SAAyC;EAC7C,MAAM,MAAM,mBAAmB;GAC7B,GAAI,QAAQ,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,QAAQ,UAAU;GAC1E,GAAI,QAAQ,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,QAAQ,IAAI;EAC1D,CAAC;EACD,OAAO;GACL,QAAQ,YAAY,IAAI,MAAM,OAAO;GAErC,aAAa,KAAA;EACf;CACF;AACF;;;;;;;;AC3VA,SAAS,YAAY,UAA2B;CAC9C,IAAI;EACF,OAAOC,KAAG,SAAS,QAAQ,CAAC,CAAC,OAAO;CACtC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,iBAAiB,cAAsB,KAA8B;CACnF,IAAI;CACJ,IAAI;EACF,UAAUA,KAAG,aAAa,cAAc,MAAM;CAChD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,SAAS,iBAAiB,cAAc,OAAO,CAAC;CACtD,MAAM,QAAQ,YAAY,MAAM,CAAC,CAC9B,QAAQ,UAAU,YAAY,MAAM,IAAI,CAAC,CAAC,CAC1C,KAAK,WAAW;EACf,MAAM,MAAM;EACZ,SAAS,KAAK,SAAS,KAAK,MAAM,IAAI,KAAK,MAAM;EACjD,MAAM,MAAM;EACZ,IAAI,MAAM;EACV,OAAO,MAAM;EACb,UAAU,WAAW,aAAa,QAAQ,MAAM,IAAI,CAAC;CACvD,EAAE;CAEJ,IAAI;EACF,MAAM,aAAaA,KAAG,aAAa,KAAK,KAAK,KAAK,aAAa,GAAG,MAAM;EACxE,OAAO,uBAAuB,OAAO,UAAU;CACjD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,aAAa,OAAqD;CACzE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAS,MAA8B,KAAK;AAC1G;;;;;AAMA,SAAgB,qBAAqC;CACnD,OAAO;EACL,WAAW;EACX,MAAM,MAAM;GACV,MAAM,yBAAS,IAAI,IAA6B;GAChD,MAAM,gCAAgB,IAAI,IAAwB;GAClD,MAAM,QAAQ,IAAI,cAAc;GAwChC,OAAO;IAtCL,WAAW,OAAO;KAChB,MAAM,QAAQ,OAAO,IAAI,MAAM,SAAS;KACxC,OAAO,UAAU,KAAA,IAAY,KAAA,IAAY,EAAE,MAAM;IACnD;IACA,aAAa,OAAO;KAClB,cAAc,IAAI,MAAM,SAAS,CAAC,GAAG;KACrC,IAAI;KACJ,IAAI;MACF,eAAe,MAAM,aAAa,MAAM,KAAK,MAAM,WAAW,MAAM,SAAS,CAAC;KAChF,QAAQ,CAER;KACA,MAAM,QAAQ,iBAAiB,KAAA,IAAY,CAAC,IAAI,iBAAiB,cAAc,MAAM,GAAG;KACxF,OAAO,IAAI,MAAM,WAAW,KAAK;KACjC,KAAK,QAAQ,MAAM,WAAW,EAAE,MAAM,CAAC;KACvC,MAAM,cAAc,KAAK,aAAa,UACpC,kBACA,MAAM,YACL,YAAY;MACX,IAAI,CAAC,aAAa,OAAO,GAAG;MAC5B,OAAO,IAAI,MAAM,WAAW,QAAQ,KAAK;MACzC,KAAK,QAAQ,MAAM,WAAW,OAAO;KACvC,GACA,EAAE,cAAc,KAAK,CACvB;KACA,cAAc,IAAI,MAAM,WAAW,WAAW;IAChD;IACA,eAAe,WAAW;KACxB,cAAc,IAAI,SAAS,CAAC,GAAG;KAC/B,cAAc,OAAO,SAAS;KAC9B,OAAO,OAAO,SAAS;IACzB;IACA,QAAQ;KACN,KAAK,MAAM,eAAe,cAAc,OAAO,GAAG,YAAY;KAC9D,cAAc,MAAM;KACpB,OAAO,MAAM;IACf;GAEiB;EACrB;CACF;AACF;;;ACjGA,MAAM,mBAAmB;AAEzB,SAAS,eAAe,KAA0D;CAChF,IAAI;EACF,MAAM,UAAU,wBAAwBC,KAAG,aAAa,KAAK,KAAK,KAAK,gBAAgB,GAAG,MAAM,CAAC;EACjG,OAAO,YAAY,KAAA,IAAY,KAAA,KAAa,aAAqB,QAAQ,KAAK,SAAS,KAAK,QAAQ,CAAC;CACvG,QAAQ;EACN;CACF;AACF;AAEA,SAAS,YAAY,OAA0C,KAAiC;CAC9F,MAAM,YAAY,MAAM;CACxB,OAAO,OAAO,cAAc,YAAY,UAAU,SAAS,IAAI,YAAY,KAAA;AAC7E;AAEA,SAAgB,sBAAsB,EAAE,OAAO,QAA0D;CACvG,IAAI,CAAC,OAAO,OAAO,EAAE,KAAK,CAAC,GAAG,EAAE;CAChC,MAAM,QAAQ,IAAI,cAAc;CAChC,MAAM,WAAW,IAAI,cAAc;CACnC,MAAM,YAAY,IAAI,yBAAyB;CAC/C,MAAM,cAAc,IAAI,YAAY,UAAU,WAAW,IAAI,wBAAwB,GAAG,EACtF,KAAK,IAAI,qBAAqB,EAChC,CAAC;CACD,IAAI,KAAK,QAAQ,iBAAiB,KAAA,GAChC,MAAM,IAAI,MAAM,iEAAiE;CACnF,MAAM,eAAe,KAAK,QAAQ;CAClC,IAAI;;;;;;;;;CAeJ,MAAM,WAAW,YAA8C;EAC7D,MAAM,QAAQ,iBAAiB,QAAQ,cAAc,QAAQ,QAAQ,GAAG;EACxE,aAAa,QAAQ,kBAAkB,QAAQ,QAAQ,WAAW,EAAE,MAAM,CAAC;EAC3E,QAAQ,QAAQ,OAAO,UACrB,gBACA,MAAM,WAAW,IAAI,KAAK,GAAG,MAAM,OAAO,GAAG,MAAM,WAAW,IAAI,SAAS,SAC7E;CACF;CA8CA,OAAO;EAAE,KAAK,CAAC,GAAG;EAAG,YAAY,CAAC;GA5ChC,MAAM;GACN,OAAO,OAAO,qBAAqB;IACjC,MAAM,aAAa,MAAM,WAAW,iBAAiB,SAAS;IAC9D,MAAM,eAAe,MAAM,aAAa,iBAAiB,KAAK,UAAU;IACxE,MAAM,gBAAgB,MAAM,cAAc,iBAAiB,KAAK,UAAU;IAC1E,SAAS,WAAW,YAAY;IAChC,UAAU,WAAW,aAAa;IAClC,MAAM,YAAY,eAAe,iBAAiB,GAAG;IACrD,YAAY,MAAM;KAChB,SAAS;MAAC;MAAc,GAAG,aAAa;MAAQ;KAAa;KAC7D,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;IACjD,CAAC;IACD,MAAM,UAAU;KAAE,SAAS;KAAkB;KAAc;IAAc;IACzE,SAAS;IACT,QAAQ,OAAO;IACf,OAAO,YAAY;KACjB,IAAI,WAAW,SAAS;KACxB,SAAS,KAAA;KACT,iBAAiB,OAAO,UAAU,gBAAgB,KAAA,CAAS;KAC3D,YAAY,MAAM;IACpB;GACF;EAuBuC,CAAC;EAAG,OAAO,CAAC;GApBnD,OAAO;GACP,QAAQ,OAAO,OAAO,qBAAqB;IACzC,MAAM,UAAU;IAChB,MAAM,aAAa,YAAY,OAAO,YAAY;IAClD,MAAM,WAAW,YAAY,OAAO,UAAU;IAC9C,IAAI,YAAY,KAAA,KAAa,QAAQ,QAAQ,cAAc,iBAAiB,aAAa,CAAC,cAAc,CAAC,UACvG;IACF,MAAM,YAAY,MAAM,YAAY,UAAU,MAAM,MAAM,iBAAiB,GAAG;GAChF;EAY2D,GAAG;GAT9D,OAAO;GACP,QAAQ,OAAO,OAAO,qBAAqB;IACzC,MAAM,UAAU;IAChB,MAAM,aAAa,YAAY,OAAO,YAAY;IAClD,IAAI,YAAY,KAAA,KAAa,QAAQ,QAAQ,cAAc,iBAAiB,aAAa,CAAC,YAAY;IACtG,MAAM,YAAY,IAAI,YAAY,MAAM,YAAY,MAAM,iBAAiB,GAAG;IAC9E,IAAI,WAAW,SAAS,QAAQ,OAAO;GACzC;EAEoE,CAAC;CAAE;AAC3E;;;AClHA,MAAa,4BAA4B,YAAqC;CAC5E,MAAM,UAAU,sBAAsB,OAAO;CAC7C,OAAO;EAAE,OAAO;EAAS,YAAY,QAAQ;CAAW;AAC1D;;;ACFA,IAAA,sBAAe,WAAW,wBAAwB;;;ACAlD,IAAA,2BAAe,cAAc,SAAS,mBAAmC;CACvE,OAAO,mBAAmB;AAC5B,CAAC;;;;;;;;;;;ACMD,IAAA,uBAAe,YAAY,GAAG;;;ACR9B,IAAA,oCAAe,YAAY,YAAoE,QAAQ,KAAK,MAAO,EAAG;;;ACAtH,IAAA,sCAAe,YAAY,YAAoE,QAAQ,KAAK,MAAO,EAAG;;;ACOtH,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAa,QAAQ,mBAAmB;CACtC,MAAM;CACN,eAAe,EACb,IAAI,WAAgD;EAAE,OAAO,OAAO,IAAI,EAAE,WAAW,aAAa,GAAGC,yBAAiB,CAAC,CAAC;CAAG,EAC7H;CACA,kBAAkB,EAChB,IAAI,WAAgD;EAAE,OAAO,OAAO,IAAI,EAAE,WAAW,aAAa,GAAGA,yBAAiB,CAAC,CAAC;CAAG,EAC7H;CACA,SAAS,OAAO,YAAY;EAC1B,MAAM,eAAe,MAAMC,oBAAY,OAAO;EAC9C,MAAM,iBAAiB;GAAE,GAAG;GAAS,MAAM,aAAa;EAAM;EAC9D,OAAO;GACL,GAAG,iBAAiB,YAAY;GAChC,IAAI,MAAsC;IAAE,OAAO,CAAC,GAAGC,sBAAU,cAAc,CAAC;GAAG;GACnF,IAAI,WAAgD;IAAE,OAAO,OAAO,IAAI,EAAE,WAAW,aAAa,GAAGF,yBAAiB,CAAC,CAAC;GAAG;GAC3H,IAAI,QAA0C;IAAE,OAAO,CAAC,IAAI,EAAE,OAAO,qBAAqB,GAAG,GAAGG,mCAAsB,cAAc,CAAC,GAAG,IAAI,EAAE,OAAO,uBAAuB,GAAG,GAAGC,qCAAwB,cAAc,CAAC,CAAC;GAAG;GAC7N,UAAU,CAAC,GAAI,aAAa,YAAY,CAAC,CAAE;GAC3C,IAAI,aAAoD;IAAE,OAAO,CAAC,GAAI,aAAa,cAAc,CAAC,CAAE;GAAG;EACzG;CACF;AACF,CAAC"}
|
package/dist/extensions/web.mjs
CHANGED
|
@@ -1472,10 +1472,10 @@ var files_web_default = defineLeaderBinding({
|
|
|
1472
1472
|
});
|
|
1473
1473
|
//#endregion
|
|
1474
1474
|
//#region generated/web.ts
|
|
1475
|
-
const via = (identity, value) =>
|
|
1475
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1476
1476
|
...identity,
|
|
1477
1477
|
...value
|
|
1478
|
-
}
|
|
1478
|
+
};
|
|
1479
1479
|
const webPlugin = defineWebPlugin({
|
|
1480
1480
|
id: "files",
|
|
1481
1481
|
session: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.mjs","names":["isRecord","routes","MediaPreview","MediaPreview","activityGroupFiles","channelFileEdits","fileLinksFiles","fillFiles","leaderFiles"],"sources":["../../src/types/webFiles.ts","../../src/extensions/workspaces/sessions/(frontend)/activity-group/files.web.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesStore.ts","../../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.web.ts","../../src/types/apiRoutes.ts","../../generated/client.ts","../../src/types/fileEditsApi.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesApi.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/fileView.ts","../../src/extensions/workspaces/sessions/(frontend)/_components/CommentDraft.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/DeleteFileDialog.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/DiffView.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/SessionMediaPreview.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/FilePanel.tsx","../../src/extensions/workspaces/sessions/(frontend)/file-links/_components/FilePreviewPanel.tsx","../../src/extensions/workspaces/sessions/(frontend)/file-links/_lib/fileLinks.ts","../../src/extensions/workspaces/sessions/(frontend)/file-links/files.web.ts","../../src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesBrowser.tsx","../../src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesActivitySection.tsx","../../src/extensions/workspaces/sessions/(frontend)/fill/Files.activity.files.web.tsx","../../src/extensions/workspaces/sessions/(frontend)/leader/files.web.ts","../../generated/web.ts"],"sourcesContent":["import type { FileEditTool } from './domain';\n\n/**\n * View types shared by this package's hub channel and its web plugin. Wire\n * JSON only, no node types, so the cockpit bundle can carry them; the\n * cockpit receives them as 'file_edits' channel payloads.\n *\n * The channel carries the list alone. One file's diffs and content are large\n * and only wanted once a reader opens the file, so those come from the session\n * API instead (see fileEditsApi.ts).\n */\n\n/** One file this session changed, as the activity dock lists it. */\nexport interface FilesItemView {\n /** Absolute path, and the identity every API route takes back. */\n path: string;\n /** Path relative to the session's working directory, which is what a row shows. */\n relPath: string;\n /** The tool behind the most recent change. */\n tool: FileEditTool;\n /** When that change happened, epoch milliseconds. */\n at: number;\n /** How many changes this session recorded for the file. */\n count: number;\n /**\n * Whether any change captured a baseline, so a diff can be shown at all. A\n * file found by comparing the tree around a bash call has no \"before\", so it\n * is listed and readable but never diffed.\n */\n diffable: boolean;\n}\n\n/** The frame type the hub publishes and the web plugin claims; declared in the doompiWeb block too. */\nexport const filesChannelType = 'file_edits';\n\n/** Footer status key whose presence shows the activity group; the extension publishes it. */\nexport const filesStatusKey = 'doom-file-edit-files';\n","import { defineActivityGroup } from '@agimon-ai/doompi-core/web';\n\nimport { filesStatusKey } from '../../../../../types/webFiles';\nexport default defineActivityGroup({\n keys: 'e f',\n statusKey: filesStatusKey,\n hideWhenEmpty: true,\n marksBackgroundWork: false,\n order: 15,\n});\n","import { defineSessionStore } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView } from '../../../../../types/fileEditsApi';\nimport { filesChannelType, type FilesItemView } from '../../../../../types/webFiles';\nimport type { FileComment } from './fileView';\n\n/**\n * One session's record: the hub's last report plus whatever this page does with\n * it. Records are immutable values; the reducer and every action return a new one.\n *\n * Detail is cached per file rather than per open tab, because a reader closes a\n * file and comes back to it, and refetching the same history for the same click\n * is work the session already did.\n */\nexport interface FilesSession {\n items: FilesItemView[];\n /** One file's fetched history, keyed by absolute path. */\n detail: Readonly<Record<string, FileEditsDetailView>>;\n /** Paths with a detail request in flight, so a panel can say it is loading. */\n loading: readonly string[];\n /** What a failed fetch or a refused save reported, keyed by absolute path. */\n errors: Readonly<Record<string, string>>;\n /** Review notes waiting to be sent, in the order they were written. */\n comments: readonly FileComment[];\n}\n\nconst empty: FilesSession = { items: [], detail: {}, loading: [], errors: {}, comments: [] };\n\nexport const files = defineSessionStore<FilesSession>(empty);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isItem(value: unknown): value is FilesItemView {\n return (\n isRecord(value) &&\n typeof value.path === 'string' &&\n typeof value.relPath === 'string' &&\n typeof value.tool === 'string' &&\n typeof value.at === 'number' &&\n typeof value.count === 'number'\n );\n}\n\nexport interface FilesPayload {\n items: FilesItemView[];\n}\n\n/** The plugin's session data channel: 'file_edits' payloads into the store. */\nexport const filesChannel = files.channel<FilesPayload>({\n channel: filesChannelType,\n parse(input) {\n if (!isRecord(input) || !Array.isArray(input.items)) return null;\n return { items: input.items.filter(isItem) };\n },\n // Only the list is replaced. The cached detail and the pending notes belong\n // to files a reader opened, not to the list: a file the session deletes stops\n // being listed, and pruning here would blank the tab still open on it and\n // then refetch it on the next frame.\n reduce: (current, { items }) => ({ ...current, items }),\n});\n\nexport function markLoading(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) =>\n current.loading.includes(filePath)\n ? current\n : { ...current, loading: [...current.loading, filePath], errors: withoutKey(current.errors, filePath) },\n );\n}\n\nexport function storeDetail(sessionId: string, detail: FileEditsDetailView): void {\n files.update(sessionId, (current) => ({\n ...current,\n detail: { ...current.detail, [detail.path]: detail },\n loading: current.loading.filter((path) => path !== detail.path),\n errors: withoutKey(current.errors, detail.path),\n }));\n}\n\nexport function storeError(sessionId: string, filePath: string, message: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n loading: current.loading.filter((path) => path !== filePath),\n errors: { ...current.errors, [filePath]: message },\n }));\n}\n\nexport function addComment(sessionId: string, comment: FileComment): void {\n files.update(sessionId, (current) => ({ ...current, comments: [...current.comments, comment] }));\n}\n\nexport function removeComment(sessionId: string, id: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.id !== id),\n }));\n}\n\nexport function clearComments(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.path !== filePath),\n }));\n}\n\nfunction withoutKey(source: Readonly<Record<string, string>>, key: string): Record<string, string> {\n return Object.fromEntries(Object.entries(source).filter(([entry]) => entry !== key));\n}\n","import { defineChannelFile } from '@agimon-ai/doompi-core/web';\n\nimport { filesChannel } from '../_lib/filesStore';\nexport default defineChannelFile(filesChannel);\n","import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { createApiClient, sessionApiAddress } from '@agimon-ai/doompi-core/web';\nimport { sealedTransport } from '@agimon-ai/doompi-web-security/browser';\n\nimport routes from '../src/types/apiRoutes';\n\n/** Scopes the tree mounts 'file-edits' at. */\nexport const apiScopes = ['session'] as const;\n\nexport const api = createApiClient(routes, {\n scopes: apiScopes,\n basePath: 'file-edits',\n transport: (input, init) => sealedTransport.fetch(input, init),\n sessionAddress: sessionApiAddress,\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","import { api } from '../../../../../../generated/client';\nimport {\n type FileEditsDetailView,\n type FileEditsPreviewView,\n PATH_QUERY_PARAM,\n} from '../../../../../types/fileEditsApi';\n\n/**\n * The page's half of this package's session API: one file's history, the\n * manual save, and the deletion.\n *\n * These are adapters now, not a transport. The generated client owns the URL\n * and the sealed transport with it, so nothing here spells a route or reaches\n * for `fetch`. What stays is this package's own vocabulary: which refusals the\n * reader is shown, and the stale-save contract the editor depends on.\n *\n * Sealing still matters and still happens, one layer down. What travels here is\n * file contents in both directions, which is the worst thing in the cockpit to\n * hand a tunnel's relay in the clear.\n */\n\nconst UNREACHABLE = 'The session is unreachable.';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Where a file's raw bytes are served from, for the ones the page shows rather\n * than edits.\n *\n * This is the host's route, not this package's, which is why the route table\n * marks it `host: true` and it lands beside the session rather than under\n * `/plugins`. The cockpit already serves any file under a session's working\n * directory, and that route carries the traversal guard, the size cap and the\n * content-type mapping. A second one here would duplicate a security boundary\n * to avoid duplicating a URL, which is the wrong way round.\n */\nexport function sessionFileUrl(sessionId: string, relPath: string): string {\n return api.session(sessionId).file.url({ query: { [PATH_QUERY_PARAM]: relPath } });\n}\n\n/** The message a reader is shown: the route's own words, or why there were none. */\nfunction messageOf(result: { status: number; error: string }): string {\n if (result.status === 0) return UNREACHABLE;\n return result.error === '' ? `The session answered ${result.status}.` : result.error;\n}\n\nexport type FetchDetailResult = { ok: true; detail: FileEditsDetailView } | { ok: false; error: string };\n\nexport async function fetchFileDetail(sessionId: string, filePath: string): Promise<FetchDetailResult> {\n const result = await api.session(sessionId).detail({ query: { [PATH_QUERY_PARAM]: filePath } });\n if (!result.ok) return { ok: false, error: messageOf(result) };\n if (!isRecord(result.data)) return { ok: false, error: 'The session answered with no detail.' };\n return { ok: true, detail: result.data };\n}\n\nexport type FetchPreviewResult = { ok: true; preview: FileEditsPreviewView } | { ok: false; error: string };\n\n/** One file the session never changed, read only; the route bounds it to the working directory. */\nexport async function fetchFilePreview(sessionId: string, filePath: string): Promise<FetchPreviewResult> {\n const result = await api.session(sessionId).preview({ query: { [PATH_QUERY_PARAM]: filePath } });\n if (!result.ok) return { ok: false, error: messageOf(result) };\n if (!isRecord(result.data)) return { ok: false, error: 'The session answered with no file.' };\n return { ok: true, preview: result.data };\n}\n\nexport type SaveResult =\n | { ok: true; hash: string }\n /** The file moved under the editor; `hash` is what it holds now. */\n | { ok: false; stale: true; error: string; hash?: string }\n | { ok: false; stale: false; error: string };\n\nexport async function saveFileContent(\n sessionId: string,\n filePath: string,\n expectedHash: string,\n content: string,\n): Promise<SaveResult> {\n const result = await api.session(sessionId).save({ body: { path: filePath, expectedHash, content } });\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. A moved hash is the one refusal the\n // editor recovers from, so it is read before the generic failure.\n if (result.status === 409) {\n const data = result.ok ? undefined : result.data;\n const hash = isRecord(data) && typeof data.hash === 'string' ? data.hash : undefined;\n return {\n ok: false,\n stale: true,\n error: result.ok ? 'The file changed since it was opened.' : messageOf(result),\n ...(hash === undefined ? {} : { hash }),\n };\n }\n if (!result.ok) return { ok: false, stale: false, error: messageOf(result) };\n return { ok: true, hash: isRecord(result.data) && typeof result.data.hash === 'string' ? result.data.hash : '' };\n}\n\nexport type DeleteResult = { ok: true } | { ok: false; error: string };\n\n/**\n * Removes the file from disk.\n *\n * The route takes only a path, with no hash to prove the reader saw what is\n * there now: a deletion discards the file whatever it holds, so a staleness\n * check would guard nothing. The confirmation in front of it is the guard.\n */\nexport async function deleteFile(sessionId: string, filePath: string): Promise<DeleteResult> {\n const result = await api.session(sessionId).remove({ query: { [PATH_QUERY_PARAM]: filePath } });\n return result.ok ? { ok: true } : { ok: false, error: messageOf(result) };\n}\n","import { grammarKeyOf, mediaKindOf } from '@agimon-ai/doompi-web-components';\n\nimport type { FileEditTool } from '../../../../../types/domain';\nimport type { FileEditsDiffHunk } from '../../../../../types/fileEditsApi';\nimport type { FilesItemView } from '../../../../../types/webFiles';\n\n/**\n * Pure view logic the file surfaces share: how a change is labelled, how a\n * file is identified, and how a set of notes becomes one message.\n *\n * None of this touches React or the DOM beyond what it is handed, so the\n * awkward parts (wording a review, sizing a gutter) are pinned by tests rather\n * than by clicking through a browser.\n */\n\n/** One review note anchored to source lines, or to a quoted rendered selection. */\nexport interface FileComment {\n id: string;\n path: string;\n relPath: string;\n startLine?: number;\n endLine?: number;\n snippet: string;\n body: string;\n}\n\n/** What a row says about the tool behind a change. */\nexport const TOOL_LABEL: Readonly<Record<FileEditTool, string>> = {\n edit: 'edit',\n write: 'write',\n bash: 'command',\n user: 'you',\n};\n\n/** Filters visible relative paths without changing the timeline's newest-first order. */\nexport function filterFileItems(items: readonly FilesItemView[], query: string): FilesItemView[] {\n const normalized = query.trim().toLocaleLowerCase();\n if (normalized.length === 0) return [...items];\n return items.filter((item) => item.relPath.toLocaleLowerCase().includes(normalized));\n}\n\n/** One header row and the files under it: the drawer's two-level grouping of a flat path list. */\nexport interface FileGroup {\n /** The shared directory the header names, or empty for files at the repository root. */\n prefix: string;\n items: FilesItemView[];\n}\n\n/** The directory part of a relative path, or empty for a file at the root. */\nfunction parentDir(relPath: string): string {\n const cut = relPath.lastIndexOf('/');\n return cut === -1 ? '' : relPath.slice(0, cut);\n}\n\n/** The longest directory two paths share, compared segment by segment rather than character by character. */\nfunction commonDir(left: string, right: string): string {\n const leftParts = left.split('/');\n const rightParts = right.split('/');\n const shared: string[] = [];\n for (let index = 0; index < Math.min(leftParts.length, rightParts.length); index += 1) {\n if (leftParts[index] !== rightParts[index]) break;\n shared.push(leftParts[index]);\n }\n return shared.join('/');\n}\n\n/**\n * Groups a flat list into one header per top-level segment, each headed by the\n * deepest directory that group shares.\n *\n * Two levels rather than a full tree: paths here run five and six segments\n * deep, so a node per segment spends more rows on directories than on the\n * files someone opened the drawer to find. The header carries the shared run\n * of directories and each row carries what is left, so a file stays one row\n * and the part that distinguishes it stays readable.\n *\n * Ordering is by path, not by recency, because a header only means anything\n * when its files sit together underneath it. The caller marks the newest\n * change instead.\n */\nexport function groupFileItems(items: readonly FilesItemView[]): FileGroup[] {\n const buckets = new Map<string, FilesItemView[]>();\n for (const item of items) {\n const cut = item.relPath.indexOf('/');\n const key = cut === -1 ? '' : item.relPath.slice(0, cut);\n const bucket = buckets.get(key);\n if (bucket === undefined) buckets.set(key, [item]);\n else bucket.push(item);\n }\n const groups: FileGroup[] = [];\n for (const [key, bucket] of buckets) {\n const sorted = [...bucket].sort((left, right) => left.relPath.localeCompare(right.relPath));\n // Root files have no directory to name, so they keep an empty header and\n // render their own paths in full.\n const prefix = key === '' ? '' : sorted.map((item) => parentDir(item.relPath)).reduce(commonDir);\n groups.push({ prefix, items: sorted });\n }\n return groups.sort((left, right) => left.prefix.localeCompare(right.prefix));\n}\n\n/** What one row shows once its group's header has already said the shared part. */\nexport function groupRowLabel(prefix: string, relPath: string): string {\n return prefix === '' ? relPath : relPath.slice(prefix.length + 1);\n}\n\n/**\n * How the preview shows a file: as the thing it is, wherever the browser can.\n *\n * One value rather than a chain of checks at the call site, because the order\n * carries the decisions. Media comes first, so a PNG the snapshot store\n * refused as binary is still a picture rather than an apology. `unavailable`\n * comes next, since a file with no readable text has nothing for the rest of\n * the list to render. Then the two documents that have a rendering of their\n * own, and then code, which is shown highlighted rather than as flat text.\n */\nexport type PreviewMode = 'media' | 'unavailable' | 'markdown' | 'html' | 'code' | 'text';\n\nexport function previewModeOf(filePath: string, unavailable: boolean): PreviewMode {\n if (mediaKindOf(filePath) !== 'download') return 'media';\n if (unavailable) return 'unavailable';\n const lower = filePath.toLowerCase();\n if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'markdown';\n if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'html';\n return grammarKeyOf(filePath) === undefined ? 'text' : 'code';\n}\n\n/** A short, stable fingerprint of a string; enough to separate two paths in an id. */\nfunction fingerprint(value: string): string {\n let hash = 2166136261;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n return (hash >>> 0).toString(36);\n}\n\n/** The transient tab id for one file; stable, so reopening focuses rather than duplicates. */\nexport function fileTabId(filePath: string): string {\n // The id reaches a URL, so anything outside the safe set becomes a dash, and\n // the path's own fingerprint keeps two similarly-named files apart.\n const slug = filePath.replaceAll(/[^a-zA-Z0-9]+/gu, '-').replace(/^-|-$/gu, '');\n return `files-file-${fingerprint(filePath)}-${slug.slice(-40)}`;\n}\n\n/** The widest line number in a diff, in characters, which sizes the gutter. */\nexport function gutterWidth(hunks: readonly FileEditsDiffHunk[]): number {\n let widest = 1;\n for (const hunk of hunks) {\n for (const row of hunk.rows) widest = Math.max(widest, String(row.line).length);\n }\n return widest;\n}\n\n/** Trims a quoted snippet to something a prompt can carry without drowning it. */\nexport function trimSnippet(snippet: string, maxLines = 20, maxChars = 2000): string {\n const lines = snippet.split('\\n');\n const clipped = lines.length > maxLines ? [...lines.slice(0, maxLines), '…'] : lines;\n const text = clipped.join('\\n');\n return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;\n}\n\n/** How a comment names the place it is about. */\nexport function commentAnchor(comment: FileComment): string {\n if (comment.startLine === undefined) return comment.relPath;\n if (comment.endLine === undefined || comment.endLine === comment.startLine) {\n return `${comment.relPath}:${comment.startLine}`;\n }\n return `${comment.relPath}:${comment.startLine}-${comment.endLine}`;\n}\n\n/**\n * The one message a review sends.\n *\n * Every note goes in a single prompt rather than one message each: N notes then\n * cost one turn, and the agent sees the whole review before it changes\n * anything, which is the difference between addressing a review and reacting to\n * its first line.\n */\nexport function buildReviewPrompt(comments: readonly FileComment[]): string {\n if (comments.length === 0) return '';\n const heading =\n comments.length === 1\n ? 'I left one review comment on a file you changed. Please address it.'\n : `I left ${comments.length} review comments on files you changed. Please address them.`;\n const body = comments\n .map((comment) => {\n const quoted = trimSnippet(comment.snippet)\n .split('\\n')\n .map((line) => `> ${line}`)\n .join('\\n');\n return `### ${commentAnchor(comment)}\\n\\n${quoted}\\n\\n${comment.body.trim()}`;\n })\n .join('\\n\\n');\n return `${heading}\\n\\n${body}`;\n}\n","import { Button, Textarea } from '@agimon-ai/doompi-web-components';\nimport { useState } from 'react';\n\nimport { trimSnippet } from '../_lib/fileView';\n\n/**\n * The box a selection raises: what was highlighted, and a note about it.\n *\n * The note is held here rather than in the session store because it belongs to\n * one open box and dies with it; only a submitted comment is worth surviving a\n * re-render, and that goes to the store.\n */\nexport interface CommentDraftProps {\n snippet: string;\n /** Absent when the selection came from a rendered preview, where lines cannot be recovered. */\n startLine?: number;\n endLine?: number;\n onSubmit: (body: string) => void;\n onCancel: () => void;\n}\n\nexport function CommentDraft({ snippet, startLine, endLine, onSubmit, onCancel }: CommentDraftProps) {\n const [body, setBody] = useState('');\n const range =\n startLine === undefined\n ? 'no line anchor: this selection came from the rendered preview'\n : endLine === undefined || endLine === startLine\n ? `line ${startLine}`\n : `lines ${startLine} to ${endLine}`;\n\n return (\n <div data-testid=\"files-comment-draft\" className=\"flex flex-col gap-1.5 border-t border-doom-border p-2\">\n <p className=\"text-2xs text-doom-faint\">{range}</p>\n <pre className=\"max-h-24 overflow-y-auto whitespace-pre-wrap break-words rounded border border-doom-border-soft bg-doom-deep p-1.5 font-mono text-xs text-doom-dim\">\n {trimSnippet(snippet, 8, 600)}\n </pre>\n <Textarea\n data-testid=\"files-comment-body\"\n value={body}\n rows={3}\n autoFocus\n placeholder=\"what should change here?\"\n onChange={(event) => setBody(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Escape') onCancel();\n if (event.key !== 'Enter' || !(event.metaKey || event.ctrlKey)) return;\n event.preventDefault();\n if (body.trim() !== '') onSubmit(body);\n }}\n className=\"text-sm\"\n />\n <div className=\"flex items-center gap-1.5\">\n <Button\n variant=\"outline\"\n size=\"xs\"\n data-testid=\"files-comment-add\"\n disabled={body.trim() === ''}\n onClick={() => onSubmit(body)}\n className=\"text-2xs\"\n >\n add comment\n </Button>\n <Button variant=\"ghost\" size=\"xs\" data-testid=\"files-comment-cancel\" onClick={onCancel} className=\"text-2xs\">\n cancel\n </Button>\n <span className=\"text-2xs text-doom-faint\">⌘⏎ to add</span>\n </div>\n </div>\n );\n}\n","import {\n Button,\n Dialog,\n DialogBody,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@agimon-ai/doompi-web-components';\n\n/**\n * The confirmation that stands between the delete button and the unlink.\n *\n * Deleting is the one action here with nothing behind it: a manual save can be\n * undone from the file's own history, and a comment can be removed before it\n * is sent, but this session's snapshots go when the session does, so a file\n * removed by mistake is not coming back from anything the cockpit holds.\n */\nexport interface DeleteFileDialogProps {\n relPath: string;\n open: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n}\n\nexport function DeleteFileDialog({ relPath, open, onConfirm, onCancel }: DeleteFileDialogProps) {\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onCancel();\n }}\n >\n <DialogContent width=\"sm\" data-testid=\"files-delete-dialog\" aria-describedby={undefined}>\n <DialogHeader>\n <DialogTitle>delete file</DialogTitle>\n </DialogHeader>\n <DialogBody>\n <DialogDescription>\n this removes <span className=\"font-bold text-doom-hi\">{relPath}</span> from disk. the session keeps its\n record of the change, but the file itself is gone.\n </DialogDescription>\n <DialogFooter>\n <Button variant=\"outline\" size=\"md\" data-testid=\"files-delete-cancel\" autoFocus onClick={onCancel}>\n cancel\n </Button>\n <Button variant=\"danger\" size=\"md\" data-testid=\"files-delete-confirm\" onClick={onConfirm}>\n delete\n </Button>\n </DialogFooter>\n </DialogBody>\n </DialogContent>\n </Dialog>\n );\n}\n","import type { FileEditsDiffHunk } from '../../../../../types/fileEditsApi';\nimport { gutterWidth } from '../_lib/fileView';\n\n/**\n * A diff, drawn the way this cockpit draws every other one: a numbered gutter\n * and a background wash per row, so a change reads at a glance rather than by\n * hunting for leading punctuation.\n *\n * Hunks arrive apart rather than joined by an elision marker, so the gap\n * between two of them is a rule the component draws, not a line it has to\n * recognise in a string.\n */\n\nconst ROW_TONE: Readonly<Record<string, string>> = {\n '+': 'bg-doom-tint-green text-doom-green',\n '-': 'bg-doom-tint-red text-doom-red',\n ' ': 'text-doom-dim',\n};\n\nexport interface DiffViewProps {\n hunks: readonly FileEditsDiffHunk[];\n /** Marks the surface so a test and a selection handler can find it. */\n testId: string;\n /** Called with the selected text and the lines it covers. */\n onSelect?: (selection: { startLine: number; endLine: number; snippet: string }) => void;\n}\n\nexport function DiffView({ hunks, testId, onSelect }: DiffViewProps) {\n const gutter = gutterWidth(hunks);\n\n /**\n * Turns whatever the reader highlighted into a line range. The rows carry\n * their own numbers, so the range comes from the first and last row the\n * selection touches rather than from counting newlines in the rendered text.\n */\n const handleSelect = (): void => {\n if (onSelect === undefined) return;\n const selection = globalThis.getSelection?.();\n const text = selection?.toString() ?? '';\n if (text.trim() === '') return;\n const rows = [...document.querySelectorAll<HTMLElement>(`[data-diff-surface=\"${testId}\"] [data-diff-line]`)];\n const touched = rows.filter((row) => selection?.containsNode(row, true) === true);\n const numbers = touched\n .map((row) => Number(row.dataset.diffLine))\n .filter((line) => Number.isFinite(line) && line > 0);\n if (numbers.length === 0) return;\n onSelect({ startLine: Math.min(...numbers), endLine: Math.max(...numbers), snippet: text });\n };\n\n if (hunks.length === 0) {\n return (\n <p data-testid={`${testId}-empty`} className=\"px-2 py-1 text-xs text-doom-faint\">\n no lines changed\n </p>\n );\n }\n\n return (\n <div\n data-testid={testId}\n data-diff-surface={testId}\n onMouseUp={handleSelect}\n className=\"overflow-x-auto font-mono text-sm leading-normal\"\n >\n {hunks.map((hunk, position) => (\n <div key={`${hunk.start}-${position}`}>\n {position > 0 ? (\n <div className=\"flex items-center gap-2 px-2 py-0.5 text-2xs text-doom-faint\">\n <span className=\"h-px flex-1 bg-doom-border-soft\" />\n <span>⋯</span>\n <span className=\"h-px flex-1 bg-doom-border-soft\" />\n </div>\n ) : null}\n {hunk.rows.map((row, offset) => (\n <div\n key={`${row.marker}-${row.line}-${offset}`}\n data-diff-line={row.line}\n data-diff-marker={row.marker}\n className={`flex whitespace-pre ${ROW_TONE[row.marker] ?? 'text-doom-dim'}`}\n >\n <span\n aria-hidden\n className=\"shrink-0 select-none pr-2 pl-2 text-right text-doom-faint\"\n style={{ width: `${gutter + 2}ch` }}\n >\n {row.line}\n </span>\n <span className=\"shrink-0 select-none pr-1\">{row.marker}</span>\n <span className=\"min-w-0\">{row.content}</span>\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import { MediaPreview } from '@agimon-ai/doompi-web-components';\nimport { sealedTransport } from '@agimon-ai/doompi-web-security/browser';\nimport { useEffect, useState } from 'react';\n\n/** Media elements cannot issue the encrypted fetch required by remote session files. */\nexport function SessionMediaPreview({\n src,\n path,\n 'data-testid': testId,\n}: {\n src: string;\n path: string;\n 'data-testid'?: string;\n}) {\n const [loaded, setLoaded] = useState<{ source: string; url?: string; error?: string }>();\n useEffect(() => {\n if (src === '') return;\n const controller = new AbortController();\n let url: string | undefined;\n void (async () => {\n const response = await sealedTransport.fetch(src, { signal: controller.signal, cache: 'no-store' });\n if (!response.ok) throw new Error(`Media could not be loaded (${response.status}).`);\n const blob = await response.blob();\n if (controller.signal.aborted) return;\n url = URL.createObjectURL(blob);\n setLoaded({ source: src, url });\n })().catch((error: unknown) => {\n if (!controller.signal.aborted)\n setLoaded({ source: src, error: error instanceof Error ? error.message : String(error) });\n });\n return () => {\n controller.abort();\n if (url !== undefined) URL.revokeObjectURL(url);\n };\n }, [src]);\n const current = loaded?.source === src ? loaded : undefined;\n if (current?.url === undefined)\n return <p role={current?.error ? 'alert' : 'status'}>{current?.error ?? 'Loading media...'}</p>;\n return <MediaPreview src={current.url} path={path} data-testid={testId} />;\n}\n","import type { TransientTab, WebPluginSlotProps } from '@agimon-ai/doompi-core/web';\nimport {\n Badge,\n Breadcrumb,\n Button,\n CodeEditor,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n type EditorSelectionRange,\n KebabIcon,\n Markdown,\n} from '@agimon-ai/doompi-web-components';\nimport { useStore } from '@tanstack/react-store';\nimport { useEffect, useRef, useState } from 'react';\n\nimport type { FileEditsVersionView } from '../../../../../types/fileEditsApi';\nimport { deleteFile, fetchFileDetail, saveFileContent, sessionFileUrl } from '../_lib/filesApi';\nimport {\n addComment,\n clearComments,\n files,\n markLoading,\n removeComment,\n storeDetail,\n storeError,\n} from '../_lib/filesStore';\nimport { buildReviewPrompt, commentAnchor, fileTabId, previewModeOf, TOOL_LABEL } from '../_lib/fileView';\nimport { CommentDraft } from './CommentDraft';\nimport { DeleteFileDialog } from './DeleteFileDialog';\nimport { DiffView } from './DiffView';\nimport { SessionMediaPreview as MediaPreview } from './SessionMediaPreview';\n\n/**\n * One file's tab: what this session did to it, and what the reader wants done\n * next.\n *\n * Three views over the same file. The diff answers \"what changed\", the history\n * below it answers \"in what order\", and edit is where a reader fixes a line\n * themselves rather than asking for it. Preview exists because a changed\n * README is read, not diffed, and because a changed PDF or screenshot has no\n * text to read at all.\n *\n * The tab is transient: it belongs to the session and the page, and closing it\n * is how it ends. Its cached detail lives in the session store, so reopening\n * the same file does not refetch a history the session already answered.\n */\n\n/**\n * `preview` and `diff` are the two ways of reading the file and sit in the\n * header. `edit` is a mode entered from the menu and left by picking either\n * reading view; the draft survives the trip, so leaving to check a diff does\n * not throw away what was typed.\n */\ntype View = 'preview' | 'diff' | 'edit';\n\ninterface FilePanelProps extends WebPluginSlotProps {\n filePath: string;\n relPath: string;\n}\n\ninterface Draft {\n snippet: string;\n startLine?: number;\n endLine?: number;\n}\n\n/** How a version heads its row: what ran, when, and what it moved. */\nfunction versionSummary(version: FileEditsVersionView): string {\n const time = new Date(version.at).toLocaleTimeString();\n const counts = version.hunks === undefined ? '' : ` +${version.additions} -${version.removals}`;\n return `${time} · ${TOOL_LABEL[version.tool] ?? version.tool}${counts}`;\n}\n\nexport function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransientTab }: FilePanelProps) {\n const session = useStore(files.store, (state) => files.select(state, sessionId));\n const detail = session.detail[filePath];\n const error = session.errors[filePath];\n const loading = session.loading.includes(filePath);\n const comments = session.comments.filter((comment) => comment.path === filePath);\n\n // Preview first: clicking a file is a request to look at the file, and the\n // diff is one question about it. The diff is one click away and the counts\n // beside the toggles already say whether there is one worth opening.\n const [view, setView] = useState<View>('preview');\n const [draft, setDraft] = useState<Draft | undefined>(undefined);\n const [source, setSource] = useState<string | undefined>(undefined);\n const [expanded, setExpanded] = useState<number | undefined>(undefined);\n const [saving, setSaving] = useState(false);\n const [saveNote, setSaveNote] = useState<string | undefined>(undefined);\n const [confirmingDelete, setConfirmingDelete] = useState(false);\n // The editor reports a range on every cursor move, and the comment box takes\n // focus when it opens. Holding the latest range here and raising the box on\n // mouse or key release keeps a drag from stealing focus halfway through it,\n // and a ref rather than state keeps a cursor move from re-rendering the tab.\n const selection = useRef<EditorSelectionRange | undefined>(undefined);\n\n // One fetch per open, and again whenever the session reports the file moved:\n // the count changing is the cheapest signal that the history grew.\n const revision = session.items.find((item) => item.path === filePath)?.count ?? 0;\n useEffect(() => {\n if (sessionId === null) return;\n let cancelled = false;\n markLoading(sessionId, filePath);\n void fetchFileDetail(sessionId, filePath).then((result) => {\n if (cancelled) return;\n if (result.ok) {\n storeDetail(sessionId, result.detail);\n setSource(undefined);\n setSaveNote(undefined);\n } else storeError(sessionId, filePath, result.error);\n });\n return () => {\n cancelled = true;\n };\n }, [sessionId, filePath, revision]);\n\n const working = detail?.working;\n const content = source ?? working?.content ?? '';\n const previewMode = previewModeOf(relPath, working?.unavailable === true);\n // An image, a video or a PDF is shown rather than read as text, and it is\n // shown from the hub's own bytes route rather than from the detail payload,\n // which carries text only.\n const mediaSrc = sessionId === null ? '' : sessionFileUrl(sessionId, relPath);\n\n const openDraft = (selection: Draft): void => {\n setDraft(selection);\n };\n\n /** Raises the comment box on whatever the editor last reported, if that was a selection. */\n const openSelectionDraft = (): void => {\n const range = selection.current;\n if (range === undefined || range.text.trim() === '') return;\n openDraft({ snippet: range.text, startLine: range.startLine, endLine: range.endLine });\n };\n\n const submitDraft = (body: string): void => {\n if (sessionId === null || draft === undefined) return;\n addComment(sessionId, {\n // The anchor plus the note's position is unique within a file, and a\n // comment never outlives the session that holds it.\n id: `${filePath}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,\n path: filePath,\n relPath,\n ...(draft.startLine === undefined ? {} : { startLine: draft.startLine }),\n ...(draft.endLine === undefined ? {} : { endLine: draft.endLine }),\n snippet: draft.snippet,\n body,\n });\n setDraft(undefined);\n };\n\n const sendReview = (): void => {\n if (sessionId === null || comments.length === 0) return;\n sendSessionFrame(sessionId, { type: 'prompt', message: buildReviewPrompt(comments) });\n clearComments(sessionId, filePath);\n };\n\n const confirmDelete = async (): Promise<void> => {\n if (sessionId === null) return;\n setConfirmingDelete(false);\n const result = await deleteFile(sessionId, filePath);\n if (!result.ok) {\n storeError(sessionId, filePath, result.error);\n return;\n }\n // The file leaves the dock on the next channel frame, and this tab is now\n // a view of something that is not there, so it goes with it.\n closeTransientTab(fileTabId(filePath));\n };\n\n const save = async (): Promise<void> => {\n if (sessionId === null || working === undefined || source === undefined) return;\n setSaving(true);\n const result = await saveFileContent(sessionId, filePath, working.hash, source);\n setSaving(false);\n if (result.ok) {\n setSaveNote('saved');\n setSource(undefined);\n markLoading(sessionId, filePath);\n const refreshed = await fetchFileDetail(sessionId, filePath);\n if (refreshed.ok) storeDetail(sessionId, refreshed.detail);\n else storeError(sessionId, filePath, refreshed.error);\n return;\n }\n setSaveNote(result.error);\n };\n\n return (\n <div data-testid=\"files-file-panel\" className=\"flex min-h-0 flex-1 flex-col\">\n <header className=\"flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2\">\n <Breadcrumb path={relPath} data-testid=\"files-breadcrumb\" className=\"min-w-0 flex-1\" />\n {/* Edit is not one of the header's two toggles, so with neither lit the\n mode needs saying; picking either toggle leaves it. */}\n {view === 'edit' ? (\n <Badge tone=\"neutral\" data-testid=\"files-editing\" className=\"shrink-0 text-2xs\">\n editing\n </Badge>\n ) : null}\n {detail === undefined ? null : (\n <span className=\"shrink-0 text-2xs text-doom-faint\">\n +{detail.cumulative.additions} -{detail.cumulative.removals}\n </span>\n )}\n {/* The two ways of looking at the file sit in the header; the things\n that act on it live behind the kebab, so a destructive one is never\n a mis-click away from a view toggle. */}\n <nav className=\"flex shrink-0 items-center gap-1\">\n {(['preview', 'diff'] as const).map((entry) => (\n <Button\n key={entry}\n variant={view === entry ? 'outline' : 'ghost'}\n size=\"xs\"\n data-testid={`files-view-${entry}`}\n onClick={() => setView(entry)}\n className=\"text-2xs\"\n >\n {entry}\n </Button>\n ))}\n </nav>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n data-testid=\"files-menu\"\n title=\"file actions\"\n className=\"shrink-0 text-doom-faint hover:text-doom-hi\"\n >\n <KebabIcon className=\"h-3 w-3\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent data-testid=\"files-menu-list\">\n <DropdownMenuItem\n data-testid=\"files-edit\"\n disabled={working?.unavailable === true}\n onSelect={() => setView('edit')}\n >\n edit\n </DropdownMenuItem>\n <DropdownMenuItem\n variant=\"destructive\"\n data-testid=\"files-delete\"\n disabled={working?.unavailable === true}\n onSelect={() => setConfirmingDelete(true)}\n >\n delete\n </DropdownMenuItem>\n <DropdownMenuItem data-testid=\"files-close\" onSelect={() => closeTransientTab(fileTabId(filePath))}>\n close\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </header>\n\n <DeleteFileDialog\n relPath={relPath}\n open={confirmingDelete}\n onConfirm={() => void confirmDelete()}\n onCancel={() => setConfirmingDelete(false)}\n />\n\n {loading && detail === undefined ? (\n <p data-testid=\"files-loading\" className=\"px-4 py-3 text-sm text-doom-faint\">\n reading this file's history…\n </p>\n ) : null}\n {error === undefined ? null : (\n <p data-testid=\"files-error\" className=\"px-4 py-3 text-sm text-doom-red\">\n {error}\n </p>\n )}\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {detail === undefined ? null : view === 'diff' ? (\n <div className=\"flex flex-col\">\n {detail.cumulative.note === undefined ? null : (\n <p data-testid=\"files-cumulative-note\" className=\"px-4 py-2 text-xs text-doom-yellow\">\n {detail.cumulative.note}\n </p>\n )}\n {detail.cumulative.hunks === undefined ? null : (\n <DiffView\n hunks={detail.cumulative.hunks}\n testId=\"files-cumulative-diff\"\n onSelect={(selection) => openDraft(selection)}\n />\n )}\n <section className=\"mt-2 border-t border-doom-border\">\n <h2 className=\"px-4 py-2 text-xs font-bold tracking-wide text-doom-faint uppercase\">\n {detail.versions.length} {detail.versions.length === 1 ? 'change' : 'changes'} this session\n </h2>\n {detail.versions.map((version) => (\n <article key={version.index} data-testid={`files-version-${version.index}`}>\n <Button\n variant=\"ghost\"\n size=\"card\"\n data-testid={`files-version-toggle-${version.index}`}\n onClick={() => setExpanded(expanded === version.index ? undefined : version.index)}\n className=\"w-full gap-2 rounded-none px-4 py-1.5 hover:bg-doom-panel\"\n >\n <span className=\"flex w-full min-w-0 items-center gap-2\">\n <span className=\"shrink-0 text-2xs text-doom-faint\">#{version.index}</span>\n <span className=\"min-w-0 flex-1 truncate text-left text-xs text-doom-text\">\n {versionSummary(version)}\n </span>\n {version.origin === 'scan' ? (\n <Badge tone=\"neutral\" className=\"shrink-0 text-2xs\">\n no baseline\n </Badge>\n ) : null}\n <span className=\"shrink-0 text-2xs text-doom-faint\">\n {expanded === version.index ? '▾' : '▸'}\n </span>\n </span>\n </Button>\n {expanded !== version.index ? null : version.hunks === undefined ? (\n <p className=\"px-4 py-1.5 text-xs text-doom-faint\">{version.note ?? 'nothing to show'}</p>\n ) : (\n <DiffView\n hunks={version.hunks}\n testId={`files-version-diff-${version.index}`}\n onSelect={(selection) => openDraft(selection)}\n />\n )}\n </article>\n ))}\n </section>\n </div>\n ) : view === 'edit' ? (\n <div className=\"flex h-full flex-col gap-2 p-3\">\n {working?.unavailable === true ? (\n <p data-testid=\"files-source-unavailable\" className=\"text-sm text-doom-faint\">\n {working.reason ?? 'this file cannot be shown'}\n </p>\n ) : (\n <>\n {/* The release, not the selection, is what raises the comment\n box: the editor reports a range continuously through a drag\n and the box takes focus when it opens. */}\n <div\n className=\"min-h-[24rem] flex-1\"\n onMouseUp={openSelectionDraft}\n onKeyUp={(event) => {\n if (event.shiftKey) openSelectionDraft();\n }}\n >\n <CodeEditor\n data-testid=\"files-source\"\n value={content}\n path={relPath}\n onChange={setSource}\n onSelect={(range) => {\n selection.current = range;\n }}\n className=\"h-full rounded border border-doom-border\"\n />\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"xs\"\n data-testid=\"files-save\"\n loading={saving}\n disabled={source === undefined || saving}\n onClick={() => void save()}\n className=\"text-2xs\"\n >\n save to disk\n </Button>\n {source === undefined ? null : (\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-revert\"\n onClick={() => setSource(undefined)}\n className=\"text-2xs\"\n >\n discard edits\n </Button>\n )}\n {saveNote === undefined ? null : (\n <span\n data-testid=\"files-save-note\"\n className={saveNote === 'saved' ? 'text-2xs text-doom-green' : 'text-2xs text-doom-red'}\n >\n {saveNote}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n ) : (\n <div\n data-testid=\"files-preview\"\n data-mode={previewMode}\n className={\n previewMode === 'code'\n ? 'flex h-full flex-col gap-2 p-4 text-sm text-doom-text'\n : 'flex flex-col gap-2 p-4 text-sm text-doom-text'\n }\n onMouseUp={() => {\n // Highlighted source knows which lines were picked, so a note on\n // it is anchored. A rendered document has no honest mapping back\n // to source lines, and a note there carries its quotation and\n // says it has none.\n if (previewMode === 'code') {\n openSelectionDraft();\n return;\n }\n const text = globalThis.getSelection?.()?.toString() ?? '';\n if (text.trim() !== '') openDraft({ snippet: text });\n }}\n >\n {/* A file the editor cannot hold is still a file the reader came\n to see, so the reason is said and the bytes are shown anyway. */}\n {previewMode === 'unavailable' ? (\n <p data-testid=\"files-preview-unavailable\" className=\"text-sm text-doom-faint\">\n {working?.reason ?? 'this file cannot be shown'}\n </p>\n ) : null}\n {previewMode === 'media' || previewMode === 'unavailable' ? (\n <MediaPreview src={mediaSrc} path={relPath} data-testid=\"files-preview-media\" />\n ) : previewMode === 'markdown' ? (\n <Markdown text={content} />\n ) : previewMode === 'html' ? (\n <iframe\n data-testid=\"files-preview-html\"\n title={relPath}\n sandbox=\"\"\n srcDoc={content}\n className=\"h-[36rem] w-full rounded border border-doom-border bg-doom-deep\"\n />\n ) : previewMode === 'code' ? (\n // The same editor the edit view uses, held open. Reading code as\n // flat text is the one thing a cockpit should never make anyone\n // do, and read-only keeps the two views honestly apart.\n <CodeEditor\n data-testid=\"files-preview-code\"\n value={content}\n path={relPath}\n readOnly\n onSelect={(range) => {\n selection.current = range;\n }}\n className=\"min-h-[24rem] flex-1 rounded border border-doom-border\"\n />\n ) : (\n // Everything else is shown as it is written. Preview is the file\n // as it stands, so a file with nothing to render still has a\n // reading view rather than an empty pane.\n <pre\n data-testid=\"files-preview-text\"\n className=\"whitespace-pre-wrap break-words font-mono text-sm leading-normal text-doom-text\"\n >\n {content}\n </pre>\n )}\n </div>\n )}\n </div>\n\n {draft === undefined ? null : (\n <CommentDraft\n snippet={draft.snippet}\n {...(draft.startLine === undefined ? {} : { startLine: draft.startLine })}\n {...(draft.endLine === undefined ? {} : { endLine: draft.endLine })}\n onSubmit={submitDraft}\n onCancel={() => setDraft(undefined)}\n />\n )}\n\n {comments.length === 0 ? null : (\n <footer data-testid=\"files-review\" className=\"shrink-0 border-t border-doom-border px-3 py-2\">\n <div className=\"flex items-center gap-2 pb-1\">\n <span className=\"flex-1 text-xs font-bold text-doom-hi\">\n {comments.length} {comments.length === 1 ? 'comment' : 'comments'}\n </span>\n <Button\n variant=\"outline\"\n size=\"xs\"\n data-testid=\"files-send-review\"\n onClick={sendReview}\n className=\"text-2xs\"\n >\n send review\n </Button>\n </div>\n {comments.map((comment) => (\n <div key={comment.id} data-testid=\"files-review-comment\" className=\"flex items-start gap-2 py-0.5\">\n <span className=\"shrink-0 text-2xs text-doom-faint\">{commentAnchor(comment)}</span>\n <span className=\"min-w-0 flex-1 truncate text-xs text-doom-text\">{comment.body}</span>\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-review-remove\"\n onClick={() => {\n if (sessionId !== null) removeComment(sessionId, comment.id);\n }}\n className=\"shrink-0 text-2xs\"\n >\n remove\n </Button>\n </div>\n ))}\n </footer>\n )}\n </div>\n );\n}\n\n/** The transient tab one file opens in; the same file reopens rather than duplicating. */\nexport function fileTab(filePath: string, relPath: string): TransientTab {\n return {\n id: fileTabId(filePath),\n label: relPath.split('/').at(-1) ?? relPath,\n panel: (props) => <FilePanel {...props} filePath={filePath} relPath={relPath} />,\n };\n}\n","import type { TransientTab, WebPluginSlotProps } from '@agimon-ai/doompi-core/web';\nimport { Breadcrumb, Button, CodeEditor, Markdown } from '@agimon-ai/doompi-web-components';\nimport { useEffect, useState } from 'react';\n\nimport type { FileEditsPreviewView } from '../../../../../../types/fileEditsApi';\nimport { SessionMediaPreview as MediaPreview } from '../../_components/SessionMediaPreview';\nimport { fetchFilePreview, sessionFileUrl } from '../../_lib/filesApi';\nimport { fileTabId, previewModeOf } from '../../_lib/fileView';\n\n/**\n * A file the session never changed, opened read-only.\n *\n * The tab a changed file opens is built around its history: a diff, a version\n * list, an editor, a delete. None of that exists for a file the session only\n * read, and offering an edit view over a file this package holds no baseline\n * for would put a save behind a reader who cannot see what they are about to\n * overwrite. So this is one view, the file as it stands, and nothing else.\n *\n * Its content comes from the preview route, which is bounded by the session's\n * working directory rather than by the timeline.\n */\n\ninterface FilePreviewPanelProps extends WebPluginSlotProps {\n filePath: string;\n}\n\n/** What one fetch settled on, tagged with the file it was about. */\ninterface Loaded {\n path: string;\n preview?: FileEditsPreviewView;\n error?: string;\n}\n\nexport function FilePreviewPanel({ filePath, sessionId, closeTransientTab }: FilePreviewPanelProps) {\n // The answer carries the path it answered for, so switching files is a state\n // that does not match rather than a reset the effect has to perform.\n const [loaded, setLoaded] = useState<Loaded | undefined>(undefined);\n\n useEffect(() => {\n if (sessionId === null) return;\n let cancelled = false;\n void fetchFilePreview(sessionId, filePath).then((result) => {\n if (cancelled) return;\n setLoaded(result.ok ? { path: filePath, preview: result.preview } : { path: filePath, error: result.error });\n });\n return () => {\n cancelled = true;\n };\n }, [sessionId, filePath]);\n\n const current = loaded?.path === filePath ? loaded : undefined;\n const preview = current?.preview;\n const error = current?.error;\n // Before the answer arrives the path is all there is to name the file by,\n // and the route's own relative path replaces it once it does.\n const relPath = preview?.relPath ?? filePath;\n const working = preview?.working;\n const content = working?.content ?? '';\n const previewMode = previewModeOf(relPath, working?.unavailable === true);\n const mediaSrc = sessionId === null ? '' : sessionFileUrl(sessionId, relPath);\n\n return (\n <div data-testid=\"files-preview-panel\" className=\"flex min-h-0 flex-1 flex-col\">\n <header className=\"flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2\">\n <Breadcrumb path={relPath} data-testid=\"files-preview-breadcrumb\" className=\"min-w-0 flex-1\" />\n <span className=\"shrink-0 text-2xs text-doom-faint\">unchanged</span>\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-preview-close\"\n onClick={() => closeTransientTab(filePreviewTabId(filePath))}\n className=\"shrink-0 text-2xs\"\n >\n close\n </Button>\n </header>\n\n {error === undefined ? null : (\n <p data-testid=\"files-preview-error\" className=\"px-4 py-3 text-sm text-doom-red\">\n {error}\n </p>\n )}\n {preview === undefined && error === undefined ? (\n <p data-testid=\"files-preview-loading\" className=\"px-4 py-3 text-sm text-doom-faint\">\n reading this file…\n </p>\n ) : null}\n\n {preview === undefined ? null : (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n <div\n data-testid=\"files-preview\"\n data-mode={previewMode}\n className=\"flex h-full flex-col gap-2 p-4 text-sm text-doom-text\"\n >\n {previewMode === 'unavailable' ? (\n <p data-testid=\"files-preview-unavailable\" className=\"text-sm text-doom-faint\">\n {working?.reason ?? 'this file cannot be shown'}\n </p>\n ) : null}\n {previewMode === 'media' || previewMode === 'unavailable' ? (\n <MediaPreview src={mediaSrc} path={relPath} data-testid=\"files-preview-media\" />\n ) : previewMode === 'markdown' ? (\n <Markdown text={content} />\n ) : previewMode === 'html' ? (\n <iframe\n data-testid=\"files-preview-html\"\n title={relPath}\n sandbox=\"\"\n srcDoc={content}\n className=\"h-[36rem] w-full rounded border border-doom-border bg-doom-deep\"\n />\n ) : previewMode === 'code' ? (\n <CodeEditor\n data-testid=\"files-preview-code\"\n value={content}\n path={relPath}\n readOnly\n className=\"min-h-[24rem] flex-1 rounded border border-doom-border\"\n />\n ) : (\n <pre\n data-testid=\"files-preview-text\"\n className=\"whitespace-pre-wrap break-words font-mono text-sm leading-normal text-doom-text\"\n >\n {content}\n </pre>\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\n/**\n * The id of the read-only tab, kept apart from the changed-file tab's own id:\n * the same file can be read now and changed later, and the two tabs are not\n * the same view of it.\n */\nexport function filePreviewTabId(filePath: string): string {\n return `${fileTabId(filePath)}-preview`;\n}\n\n/** The transient tab an unchanged file opens in; the same file reopens rather than duplicating. */\nexport function filePreviewTab(filePath: string): TransientTab {\n return {\n id: filePreviewTabId(filePath),\n label: filePath.split('/').at(-1) ?? filePath,\n panel: (props) => <FilePreviewPanel {...props} filePath={filePath} />,\n };\n}\n","import type { FileLinkSource, TransientTab } from '@agimon-ai/doompi-core/web';\n\nimport type { FilesItemView } from '../../../../../../types/webFiles';\nimport { fileTab } from '../../_components/FilePanel';\nimport { files } from '../../_lib/filesStore';\nimport { filePreviewTab } from '../_components/FilePreviewPanel';\n\n/**\n * The files a message names, as links into the same tab the activity dock\n * opens.\n *\n * Only files this session recorded a change to are claimed. A message quotes a\n * great deal that is shaped like nothing in particular, class names and globs\n * and flags among it, and guessing at what is a path produces links that open\n * an error. The recorded set is the one thing the page knows for certain, and\n * it is also the set a reader is most likely to want open.\n */\n\n/** A reference the way a message writes one: 'src/app.ts:42' or 'src/app.ts:42:9'. */\nconst LINE_SUFFIX = /:\\d+(?::\\d+)?$/;\n\nfunction itemsFor(sessionId: string | null): readonly FilesItemView[] {\n return files.select(files.store.state, sessionId).items;\n}\n\nfunction match(sessionId: string | null, path: string): FilesItemView | undefined {\n const candidate = path.trim().replace(LINE_SUFFIX, '');\n if (candidate === '') return undefined;\n return itemsFor(sessionId).find((item) => item.relPath === candidate || item.path === candidate);\n}\n\nexport const fileLinks: FileLinkSource = {\n subscribe(listener: () => void) {\n const subscription = files.store.subscribe(listener);\n return () => subscription.unsubscribe();\n },\n // The paths themselves, not their count: a file that leaves the list while\n // another arrives changes what resolves without changing how many do.\n fingerprint: (sessionId) =>\n itemsFor(sessionId)\n .map((item) => item.relPath)\n .join('\\n'),\n resolve: (sessionId, path): TransientTab | undefined => {\n const item = match(sessionId, path);\n return item === undefined ? undefined : fileTab(item.path, item.relPath);\n },\n // A tool argument is a path on purpose, so nothing here has to be guessed.\n // A file the session changed opens on its history; one it only read opens\n // read-only, and the route behind that tab is what decides whether a path\n // outside the working directory may be shown at all.\n openPath: (sessionId, path): TransientTab | undefined => {\n const item = match(sessionId, path);\n if (item !== undefined) return fileTab(item.path, item.relPath);\n // A call with no path argument at all hands this an empty string, and\n // there is no file to open behind it.\n const candidate = path.trim();\n return candidate === '' ? undefined : filePreviewTab(candidate);\n },\n};\n","import { defineFileLinks } from '@agimon-ai/doompi-core/web';\n\nimport { fileLinks } from './_lib/fileLinks';\nexport default defineFileLinks(fileLinks);\n","import { Button, CloseIcon, Input } from '@agimon-ai/doompi-web-components';\nimport type { ChangeEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';\nimport { Fragment, useEffect, useRef, useState } from 'react';\n\nimport type { FilesItemView } from '../../../../../../types/webFiles';\nimport { filterFileItems, groupFileItems, groupRowLabel, TOOL_LABEL } from '../../_lib/fileView';\n\nfunction FileMetadata({ item, label, recent = false }: { item: FilesItemView; label?: string; recent?: boolean }) {\n return (\n <span className=\"flex min-w-0 w-full items-center gap-1.5\">\n {/* The dock passes no label and keeps the whole path; the drawer's tree\n passes what its group header has not already said. */}\n <span className=\"min-w-0 flex-1 truncate text-left text-xs font-bold text-doom-hi\">{label ?? item.relPath}</span>\n {recent ? (\n <span data-recent=\"true\" title=\"most recent change\" className=\"shrink-0 text-2xs text-doom-blue\">\n ●\n </span>\n ) : null}\n {item.count > 1 ? <span className=\"shrink-0 text-2xs text-doom-faint\">{item.count}×</span> : null}\n <span className={`shrink-0 text-2xs ${item.diffable ? 'text-doom-faint' : 'text-doom-yellow'}`}>\n {TOOL_LABEL[item.tool] ?? item.tool}\n </span>\n </span>\n );\n}\n\nexport function FileActivityRow({ item, onOpen }: { item: FilesItemView; onOpen: () => void }) {\n return (\n <Button\n variant=\"ghost\"\n size=\"card\"\n data-testid={`activity-file-${item.relPath}`}\n data-file-diffable={item.diffable}\n title={item.diffable ? item.path : `${item.path} (changed by a command, so no diff was captured)`}\n onClick={onOpen}\n className=\"min-w-0 gap-0.5 rounded-md px-1 py-1 hover:bg-doom-panel\"\n >\n <FileMetadata item={item} />\n </Button>\n );\n}\n\n/**\n * The directory a run of rows shares.\n *\n * Deliberately not an option: the cursor counts files, and a header is a label\n * rather than something to open. Keeping it out of the listbox is what lets\n * arrow keys and Enter stay exactly what they were before the tree existed.\n */\nfunction FileGroupHeader({ prefix }: { prefix: string }) {\n return (\n <div\n data-testid={`files-browser-group-${prefix}`}\n className=\"truncate px-4 pt-2 pb-1 text-2xs font-bold tracking-wide text-doom-faint\"\n >\n {prefix}\n </div>\n );\n}\n\nfunction BrowserFileRow({\n item,\n label,\n indented = false,\n recent = false,\n selected,\n onSelect,\n onOpen,\n}: {\n item: FilesItemView;\n label: string;\n indented?: boolean;\n recent?: boolean;\n selected: boolean;\n onSelect: () => void;\n onOpen: () => void;\n}) {\n return (\n <div\n role=\"option\"\n aria-selected={selected}\n tabIndex={-1}\n data-testid={`files-browser-file-${item.relPath}`}\n data-file-diffable={item.diffable}\n data-selected={selected}\n title={item.diffable ? item.path : `${item.path} (changed by a command, so no diff was captured)`}\n onMouseEnter={onSelect}\n onClick={onOpen}\n className={`flex min-w-0 cursor-pointer border-l-2 py-2 ${indented ? 'pr-4 pl-8' : 'px-4'} ${\n selected ? 'border-doom-blue bg-doom-panel' : 'border-transparent hover:bg-doom-panel/60'\n }`}\n >\n <FileMetadata item={item} label={label} recent={recent} />\n </div>\n );\n}\n\n/** The complete session file picker, ordered exactly as the file timeline supplies it. */\nexport function FilesBrowser({\n items,\n onClose,\n onOpen,\n}: {\n items: readonly FilesItemView[];\n onClose: () => void;\n onOpen: (item: FilesItemView) => void;\n}) {\n const [query, setQuery] = useState('');\n const [selectedIndex, setSelectedIndex] = useState(0);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const shown = filterFileItems(items, query);\n // Grouping only earns its headers with the whole list in view. While a query\n // is active the reader is pinpointing one path, so the flat list answers that\n // better than a shape wrapped around two matches.\n const grouped = query.trim() === '' ? groupFileItems(shown) : undefined;\n // The order rows actually appear in, which is what the cursor counts through.\n const ordered = grouped === undefined ? shown : grouped.flatMap((group) => group.items);\n const positions = new Map(ordered.map((item, index) => [item.path, index]));\n const selected = ordered[selectedIndex];\n // The timeline hands the newest change first, so the head of the unfiltered\n // list is the one row worth marking once the tree has reordered by path.\n const recentPath = items[0]?.path;\n\n // A shrinking filter can strand the cursor past the end, so the clamp happens\n // while rendering the shorter list rather than in a pass after it paints.\n const [lastShownLength, setLastShownLength] = useState(shown.length);\n if (lastShownLength !== shown.length) {\n setLastShownLength(shown.length);\n setSelectedIndex((current) => Math.min(current, Math.max(0, shown.length - 1)));\n }\n\n useEffect(() => {\n listRef.current?.querySelector('[aria-selected=\"true\"]')?.scrollIntoView({ block: 'nearest' });\n }, [selectedIndex]);\n\n const move = (delta: number): void => {\n if (ordered.length === 0) return;\n setSelectedIndex((current) => (current + delta + ordered.length) % ordered.length);\n };\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLElement>): void => {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n move(event.key === 'ArrowDown' ? 1 : -1);\n return;\n }\n if (event.key === 'Escape') {\n event.preventDefault();\n onClose();\n return;\n }\n if (\n event.key === 'Enter' &&\n selected !== undefined &&\n (!(event.target instanceof Element) || event.target.closest('button') === null)\n ) {\n event.preventDefault();\n onOpen(selected);\n }\n };\n\n return (\n <aside\n data-testid=\"files-browser\"\n aria-label=\"changed files\"\n tabIndex={-1}\n onKeyDown={onKeyDown}\n className=\"fixed inset-y-0 right-0 z-50 flex w-[min(440px,calc(100vw-24px))] flex-col overflow-hidden border-l border-doom-border bg-doom-rail outline-none\"\n >\n <div className=\"flex h-11 shrink-0 items-center gap-2.5 border-b border-doom-border px-4\">\n <span className=\"text-base font-bold text-doom-hi\"># files</span>\n <span data-testid=\"files-browser-total\" className=\"text-2xs text-doom-faint\">\n {items.length} changed\n </span>\n <span className=\"min-w-0 flex-1\" />\n <Button\n variant=\"ghost\"\n size=\"icon\"\n data-testid=\"files-browser-close\"\n aria-label=\"close changed files\"\n title=\"close changed files\"\n onClick={onClose}\n >\n <CloseIcon className=\"h-3 w-3\" />\n </Button>\n </div>\n <div className=\"flex items-center gap-2 px-4 pt-2.5 pb-1.5\">\n <Input\n ref={inputRef}\n data-testid=\"files-browser-search\"\n aria-label=\"search changed file paths\"\n value={query}\n placeholder=\"search paths…\"\n autoFocus\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setQuery(event.target.value);\n setSelectedIndex(0);\n }}\n />\n {query.length > 0 ? (\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-browser-clear\"\n aria-label=\"clear path search\"\n onClick={() => {\n setQuery('');\n setSelectedIndex(0);\n inputRef.current?.focus();\n }}\n >\n clear\n </Button>\n ) : null}\n </div>\n <p data-testid=\"files-browser-matches\" className=\"shrink-0 px-4 pb-2 text-2xs text-doom-faint\">\n {shown.length} matches · {grouped === undefined ? 'newest change first' : 'by path'}\n </p>\n <div\n ref={listRef}\n role=\"listbox\"\n aria-label=\"changed files\"\n className=\"flex min-h-0 flex-1 flex-col overflow-y-auto pb-2\"\n >\n {shown.length === 0 ? <p className=\"px-4 py-3 text-xs text-doom-faint\">nothing matches the path</p> : null}\n {grouped === undefined\n ? shown.map((item) => (\n <BrowserFileRow\n key={item.path}\n item={item}\n label={item.relPath}\n recent={item.path === recentPath}\n selected={positions.get(item.path) === selectedIndex}\n onSelect={() => setSelectedIndex(positions.get(item.path) ?? 0)}\n onOpen={() => onOpen(item)}\n />\n ))\n : grouped.map((group) => (\n <Fragment key={group.prefix}>\n {group.prefix === '' ? null : <FileGroupHeader prefix={group.prefix} />}\n {group.items.map((item) => (\n <BrowserFileRow\n key={item.path}\n item={item}\n label={groupRowLabel(group.prefix, item.relPath)}\n indented\n recent={item.path === recentPath}\n selected={positions.get(item.path) === selectedIndex}\n onSelect={() => setSelectedIndex(positions.get(item.path) ?? 0)}\n onOpen={() => onOpen(item)}\n />\n ))}\n </Fragment>\n ))}\n </div>\n <div className=\"flex h-8 shrink-0 items-center border-t border-doom-border-soft bg-doom-deep px-4\">\n <span className=\"text-2xs text-doom-faint\">↑↓ choose · enter open · esc close</span>\n </div>\n </aside>\n );\n}\n","import type { WebPluginSlotProps } from '@agimon-ai/doompi-core/web';\nimport { Button } from '@agimon-ai/doompi-web-components';\nimport { useStore } from '@tanstack/react-store';\nimport { useState } from 'react';\n\nimport type { FilesItemView } from '../../../../../../types/webFiles';\nimport { fileTab } from '../../_components/FilePanel';\nimport { files } from '../../_lib/filesStore';\nimport { FileActivityRow, FilesBrowser } from './FilesBrowser';\n\n/**\n * The files group's body in the activity dock: what this session has changed.\n *\n * Every recorded file is listed, not only the ones the agent named. A file\n * found by comparing the tree around a bash call is marked, because it is\n * openable and editable but has no baseline to diff against, and a row that\n * hid that distinction would promise a diff the tab cannot show.\n */\n\n/** The most recent changes first, which is the order the timeline already folds to. */\nconst VISIBLE_ROWS = 5;\n\nexport function FilesActivitySection({ sessionId, openTransientTab }: WebPluginSlotProps) {\n const { items } = useStore(files.store, (state) => files.select(state, sessionId));\n const [browserOpen, setBrowserOpen] = useState(false);\n\n const openFile = (item: FilesItemView): void => {\n if (sessionId === null) return;\n openTransientTab(fileTab(item.path, item.relPath));\n setBrowserOpen(false);\n };\n if (items.length === 0) {\n return (\n <p data-testid=\"activity-summary-files\" className=\"px-1 text-xs text-doom-faint\">\n nothing changed yet\n </p>\n );\n }\n\n const visible = items.slice(0, VISIBLE_ROWS);\n\n return (\n <div data-testid=\"activity-file-edits\" className=\"flex flex-col gap-0.5\">\n {visible.map((item: FilesItemView) => (\n <FileActivityRow key={item.path} item={item} onOpen={() => openFile(item)} />\n ))}\n {items.length > visible.length ? (\n <Button\n variant=\"link\"\n size=\"xs\"\n data-testid=\"activity-files-show-all\"\n aria-label={`show all ${items.length} changed files`}\n className=\"self-start px-1 pt-0.5 text-2xs text-doom-faint\"\n onClick={() => setBrowserOpen(true)}\n >\n show all {items.length} files\n </Button>\n ) : null}\n {browserOpen ? <FilesBrowser items={items} onClose={() => setBrowserOpen(false)} onOpen={openFile} /> : null}\n </div>\n );\n}\n","import { defineFill } from '@agimon-ai/doompi-core/web';\n\nimport { FilesActivitySection } from './_components/FilesActivitySection';\nexport default defineFill({ component: FilesActivitySection });\n","import { defineLeaderBinding } from '@agimon-ai/doompi-core/web';\nconst EXTENSION_GROUP = { key: 'e', label: 'extension', detail: 'tools, skills and config' };\nexport default defineLeaderBinding({\n path: [EXTENSION_GROUP, { key: 'f', label: 'files', detail: 'files this session changed' }],\n command: 'file-edits',\n});\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineWebPlugin, type WebPluginContributions } from '@agimon-ai/doompi-core/web';\n\nimport activityGroupFiles from '../src/extensions/workspaces/sessions/(frontend)/activity-group/files.web';\nimport channelFileEdits from '../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.web';\nimport fileLinksFiles from '../src/extensions/workspaces/sessions/(frontend)/file-links/files.web';\nimport fillFiles from '../src/extensions/workspaces/sessions/(frontend)/fill/Files.activity.files.web';\nimport leaderFiles from '../src/extensions/workspaces/sessions/(frontend)/leader/files.web';\n\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T => ({ ...identity, ...(value as object) }) as T;\n\nexport const webPlugin = defineWebPlugin({\n id: 'files',\n session: {\n get activityGroups(): WebPluginContributions['activityGroups'] { return [via({ name: 'files' }, activityGroupFiles)]; },\n get channels(): WebPluginContributions['channels'] { return [via({ channel: 'file_edits' }, channelFileEdits)]; },\n get fileLinks(): WebPluginContributions['fileLinks'] { return fileLinksFiles; },\n get fills(): WebPluginContributions['fills'] { return [via({ slot: 'activity.files', id: 'files' }, fillFiles)]; },\n get leaderBindings(): WebPluginContributions['leaderBindings'] { return [via({ id: 'files.files' }, leaderFiles)]; },\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web.mjs","names":["isRecord","routes","MediaPreview","MediaPreview","activityGroupFiles","channelFileEdits","fileLinksFiles","fillFiles","leaderFiles"],"sources":["../../src/types/webFiles.ts","../../src/extensions/workspaces/sessions/(frontend)/activity-group/files.web.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesStore.ts","../../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.web.ts","../../src/types/apiRoutes.ts","../../generated/client.ts","../../src/types/fileEditsApi.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesApi.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/fileView.ts","../../src/extensions/workspaces/sessions/(frontend)/_components/CommentDraft.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/DeleteFileDialog.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/DiffView.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/SessionMediaPreview.tsx","../../src/extensions/workspaces/sessions/(frontend)/_components/FilePanel.tsx","../../src/extensions/workspaces/sessions/(frontend)/file-links/_components/FilePreviewPanel.tsx","../../src/extensions/workspaces/sessions/(frontend)/file-links/_lib/fileLinks.ts","../../src/extensions/workspaces/sessions/(frontend)/file-links/files.web.ts","../../src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesBrowser.tsx","../../src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesActivitySection.tsx","../../src/extensions/workspaces/sessions/(frontend)/fill/Files.activity.files.web.tsx","../../src/extensions/workspaces/sessions/(frontend)/leader/files.web.ts","../../generated/web.ts"],"sourcesContent":["import type { FileEditTool } from './domain';\n\n/**\n * View types shared by this package's hub channel and its web plugin. Wire\n * JSON only, no node types, so the cockpit bundle can carry them; the\n * cockpit receives them as 'file_edits' channel payloads.\n *\n * The channel carries the list alone. One file's diffs and content are large\n * and only wanted once a reader opens the file, so those come from the session\n * API instead (see fileEditsApi.ts).\n */\n\n/** One file this session changed, as the activity dock lists it. */\nexport interface FilesItemView {\n /** Absolute path, and the identity every API route takes back. */\n path: string;\n /** Path relative to the session's working directory, which is what a row shows. */\n relPath: string;\n /** The tool behind the most recent change. */\n tool: FileEditTool;\n /** When that change happened, epoch milliseconds. */\n at: number;\n /** How many changes this session recorded for the file. */\n count: number;\n /**\n * Whether any change captured a baseline, so a diff can be shown at all. A\n * file found by comparing the tree around a bash call has no \"before\", so it\n * is listed and readable but never diffed.\n */\n diffable: boolean;\n}\n\n/** The frame type the hub publishes and the web plugin claims; declared in the doompiWeb block too. */\nexport const filesChannelType = 'file_edits';\n\n/** Footer status key whose presence shows the activity group; the extension publishes it. */\nexport const filesStatusKey = 'doom-file-edit-files';\n","import { defineActivityGroup } from '@agimon-ai/doompi-core/web';\n\nimport { filesStatusKey } from '../../../../../types/webFiles';\nexport default defineActivityGroup({\n keys: 'e f',\n statusKey: filesStatusKey,\n hideWhenEmpty: true,\n marksBackgroundWork: false,\n order: 15,\n});\n","import { defineSessionStore } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView } from '../../../../../types/fileEditsApi';\nimport { filesChannelType, type FilesItemView } from '../../../../../types/webFiles';\nimport type { FileComment } from './fileView';\n\n/**\n * One session's record: the hub's last report plus whatever this page does with\n * it. Records are immutable values; the reducer and every action return a new one.\n *\n * Detail is cached per file rather than per open tab, because a reader closes a\n * file and comes back to it, and refetching the same history for the same click\n * is work the session already did.\n */\nexport interface FilesSession {\n items: FilesItemView[];\n /** One file's fetched history, keyed by absolute path. */\n detail: Readonly<Record<string, FileEditsDetailView>>;\n /** Paths with a detail request in flight, so a panel can say it is loading. */\n loading: readonly string[];\n /** What a failed fetch or a refused save reported, keyed by absolute path. */\n errors: Readonly<Record<string, string>>;\n /** Review notes waiting to be sent, in the order they were written. */\n comments: readonly FileComment[];\n}\n\nconst empty: FilesSession = { items: [], detail: {}, loading: [], errors: {}, comments: [] };\n\nexport const files = defineSessionStore<FilesSession>(empty);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isItem(value: unknown): value is FilesItemView {\n return (\n isRecord(value) &&\n typeof value.path === 'string' &&\n typeof value.relPath === 'string' &&\n typeof value.tool === 'string' &&\n typeof value.at === 'number' &&\n typeof value.count === 'number'\n );\n}\n\nexport interface FilesPayload {\n items: FilesItemView[];\n}\n\n/** The plugin's session data channel: 'file_edits' payloads into the store. */\nexport const filesChannel = files.channel<FilesPayload>({\n channel: filesChannelType,\n parse(input) {\n if (!isRecord(input) || !Array.isArray(input.items)) return null;\n return { items: input.items.filter(isItem) };\n },\n // Only the list is replaced. The cached detail and the pending notes belong\n // to files a reader opened, not to the list: a file the session deletes stops\n // being listed, and pruning here would blank the tab still open on it and\n // then refetch it on the next frame.\n reduce: (current, { items }) => ({ ...current, items }),\n});\n\nexport function markLoading(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) =>\n current.loading.includes(filePath)\n ? current\n : { ...current, loading: [...current.loading, filePath], errors: withoutKey(current.errors, filePath) },\n );\n}\n\nexport function storeDetail(sessionId: string, detail: FileEditsDetailView): void {\n files.update(sessionId, (current) => ({\n ...current,\n detail: { ...current.detail, [detail.path]: detail },\n loading: current.loading.filter((path) => path !== detail.path),\n errors: withoutKey(current.errors, detail.path),\n }));\n}\n\nexport function storeError(sessionId: string, filePath: string, message: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n loading: current.loading.filter((path) => path !== filePath),\n errors: { ...current.errors, [filePath]: message },\n }));\n}\n\nexport function addComment(sessionId: string, comment: FileComment): void {\n files.update(sessionId, (current) => ({ ...current, comments: [...current.comments, comment] }));\n}\n\nexport function removeComment(sessionId: string, id: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.id !== id),\n }));\n}\n\nexport function clearComments(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.path !== filePath),\n }));\n}\n\nfunction withoutKey(source: Readonly<Record<string, string>>, key: string): Record<string, string> {\n return Object.fromEntries(Object.entries(source).filter(([entry]) => entry !== key));\n}\n","import { defineChannelFile } from '@agimon-ai/doompi-core/web';\n\nimport { filesChannel } from '../_lib/filesStore';\nexport default defineChannelFile(filesChannel);\n","import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport type { FileEditsDetailView, FileEditsPreviewView, FileEditsSaveView } from './fileEditsApi';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path. The build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount\n * is stated once, by the folder that creates it. They used to be written here,\n * in the contract and again inside every URL the page built, and the copies\n * were free to disagree.\n *\n * This is the one file the routes, the Hono app and the browser all read, so a\n * path cannot move on one side alone.\n */\nexport default defineApiRoutes({\n detail: {\n method: 'GET',\n path: '/detail',\n query: ['path'],\n response: apiResponse<FileEditsDetailView>(),\n },\n preview: {\n method: 'GET',\n path: '/preview',\n query: ['path'],\n response: apiResponse<FileEditsPreviewView>(),\n },\n save: {\n method: 'PUT',\n path: '/content',\n response: apiResponse<FileEditsSaveView>(),\n },\n /** The same path as `save`; the method is the whole of the difference. */\n remove: {\n method: 'DELETE',\n path: '/content',\n query: ['path'],\n },\n /**\n * The host's own bytes route, not this package's.\n *\n * `host: true` hangs it off the session root rather than under `/plugins`,\n * which is how the cockpit already serves it. Declaring it here is the first\n * time anything records that this package's previews depend on it.\n */\n file: {\n method: 'GET',\n path: '/file',\n query: ['path'],\n host: true,\n },\n});\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { createApiClient, sessionApiAddress } from '@agimon-ai/doompi-core/web';\nimport { sealedTransport } from '@agimon-ai/doompi-web-security/browser';\n\nimport routes from '../src/types/apiRoutes';\n\n/** Scopes the tree mounts 'file-edits' at. */\nexport const apiScopes = ['session'] as const;\n\nexport const api = createApiClient(routes, {\n scopes: apiScopes,\n basePath: 'file-edits',\n transport: (input, init) => sealedTransport.fetch(input, init),\n sessionAddress: sessionApiAddress,\n});\n","import type { FileEditOrigin, FileEditTool } from './domain';\n\n/**\n * The wire vocabulary this package's API shares with whatever calls it.\n *\n * It lives under src/types because that is the one server root a browser\n * bundle may read: the cockpit plugin builds its URLs from these and the\n * routes answer them, so neither half can drift from the other.\n *\n * Routes live in `./apiRoutes`, which the contract, the Hono app and the\n * generated client all read. This file holds the shapes that travel over them.\n *\n * Opening a changed file wants its whole history at once, so `detail` answers\n * in one round trip rather than making the page stitch three reads together;\n * `content` takes the manual save back; `preview` reads a file the session\n * never changed, which has no history to answer with.\n */\n\n/** Where a host mounts this package's API; the segment after /api/plugins/. */\nexport const API_BASE_PATH = 'file-edits';\n\n/** Query parameter naming the file a request is about. */\nexport const PATH_QUERY_PARAM = 'path';\n\n/**\n * One line of a diff. The number is the new file's for context and additions\n * and the old file's for removals, so it always says where the line lives.\n */\nexport interface FileEditsDiffRow {\n marker: '+' | '-' | ' ';\n line: number;\n content: string;\n}\n\n/**\n * A run of changed lines with its surrounding context. Hunks are sent apart\n * rather than joined by an elision marker, so the page draws the gap between\n * them instead of parsing one out of a string.\n */\nexport interface FileEditsDiffHunk {\n /** The line number the hunk opens at. */\n start: number;\n rows: FileEditsDiffRow[];\n}\n\n/**\n * One change in a file's history, each rendered on its own so a file edited\n * four times shows four distinct diffs rather than one merged blur.\n */\nexport interface FileEditsVersionView {\n /** Position in the file's history, oldest first, starting at 1. */\n index: number;\n tool: FileEditTool;\n at: number;\n origin: FileEditOrigin;\n additions: number;\n removals: number;\n /** The changed lines of this one change; absent when no baseline was captured. */\n hunks?: FileEditsDiffHunk[];\n /** Why there is no diff, shown in its place. */\n note?: string;\n}\n\n/** Everything this session did to the file: its first baseline against what is on disk now. */\nexport interface FileEditsCumulativeView {\n additions: number;\n removals: number;\n hunks?: FileEditsDiffHunk[];\n note?: string;\n}\n\n/** The file as it stands on disk right now, which is what the source view edits. */\nexport interface FileEditsWorkingView {\n content: string;\n /** sha256 of the content; the token a save hands back to prove it is not stale. */\n hash: string;\n /** True when the file is binary, missing, or past the size cap, so `content` is empty. */\n unavailable: boolean;\n reason?: string;\n}\n\n/** What the detail route answers with. */\nexport interface FileEditsDetailView {\n path: string;\n relPath: string;\n versions: FileEditsVersionView[];\n cumulative: FileEditsCumulativeView;\n working: FileEditsWorkingView;\n}\n\n/**\n * What the preview route answers with: the file as it stands, and nothing\n * about how it got there. A file this session never touched has no history and\n * no baseline, so there is no diff to send and no save to take back.\n */\nexport interface FileEditsPreviewView {\n path: string;\n relPath: string;\n working: FileEditsWorkingView;\n}\n\n/** What the save route takes. */\nexport interface FileEditsSaveRequest {\n path: string;\n /** The hash the reader loaded; a mismatch means the file moved underneath them. */\n expectedHash: string;\n content: string;\n}\n\n/** What the save route answers with. */\nexport interface FileEditsSaveView {\n hash: string;\n}\n\n/** What a route reports when it refuses; the page shows `error` verbatim. */\nexport interface FileEditsErrorView {\n error: string;\n /** On a stale save, the hash the file actually has now. */\n hash?: string;\n}\n","import { api } from '../../../../../../generated/client';\nimport {\n type FileEditsDetailView,\n type FileEditsPreviewView,\n PATH_QUERY_PARAM,\n} from '../../../../../types/fileEditsApi';\n\n/**\n * The page's half of this package's session API: one file's history, the\n * manual save, and the deletion.\n *\n * These are adapters now, not a transport. The generated client owns the URL\n * and the sealed transport with it, so nothing here spells a route or reaches\n * for `fetch`. What stays is this package's own vocabulary: which refusals the\n * reader is shown, and the stale-save contract the editor depends on.\n *\n * Sealing still matters and still happens, one layer down. What travels here is\n * file contents in both directions, which is the worst thing in the cockpit to\n * hand a tunnel's relay in the clear.\n */\n\nconst UNREACHABLE = 'The session is unreachable.';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Where a file's raw bytes are served from, for the ones the page shows rather\n * than edits.\n *\n * This is the host's route, not this package's, which is why the route table\n * marks it `host: true` and it lands beside the session rather than under\n * `/plugins`. The cockpit already serves any file under a session's working\n * directory, and that route carries the traversal guard, the size cap and the\n * content-type mapping. A second one here would duplicate a security boundary\n * to avoid duplicating a URL, which is the wrong way round.\n */\nexport function sessionFileUrl(sessionId: string, relPath: string): string {\n return api.session(sessionId).file.url({ query: { [PATH_QUERY_PARAM]: relPath } });\n}\n\n/** The message a reader is shown: the route's own words, or why there were none. */\nfunction messageOf(result: { status: number; error: string }): string {\n if (result.status === 0) return UNREACHABLE;\n return result.error === '' ? `The session answered ${result.status}.` : result.error;\n}\n\nexport type FetchDetailResult = { ok: true; detail: FileEditsDetailView } | { ok: false; error: string };\n\nexport async function fetchFileDetail(sessionId: string, filePath: string): Promise<FetchDetailResult> {\n const result = await api.session(sessionId).detail({ query: { [PATH_QUERY_PARAM]: filePath } });\n if (!result.ok) return { ok: false, error: messageOf(result) };\n if (!isRecord(result.data)) return { ok: false, error: 'The session answered with no detail.' };\n return { ok: true, detail: result.data };\n}\n\nexport type FetchPreviewResult = { ok: true; preview: FileEditsPreviewView } | { ok: false; error: string };\n\n/** One file the session never changed, read only; the route bounds it to the working directory. */\nexport async function fetchFilePreview(sessionId: string, filePath: string): Promise<FetchPreviewResult> {\n const result = await api.session(sessionId).preview({ query: { [PATH_QUERY_PARAM]: filePath } });\n if (!result.ok) return { ok: false, error: messageOf(result) };\n if (!isRecord(result.data)) return { ok: false, error: 'The session answered with no file.' };\n return { ok: true, preview: result.data };\n}\n\nexport type SaveResult =\n | { ok: true; hash: string }\n /** The file moved under the editor; `hash` is what it holds now. */\n | { ok: false; stale: true; error: string; hash?: string }\n | { ok: false; stale: false; error: string };\n\nexport async function saveFileContent(\n sessionId: string,\n filePath: string,\n expectedHash: string,\n content: string,\n): Promise<SaveResult> {\n const result = await api.session(sessionId).save({ body: { path: filePath, expectedHash, content } });\n // The agent runs in the same working directory, so it can rewrite the file\n // while a reader is still editing it. A moved hash is the one refusal the\n // editor recovers from, so it is read before the generic failure.\n if (result.status === 409) {\n const data = result.ok ? undefined : result.data;\n const hash = isRecord(data) && typeof data.hash === 'string' ? data.hash : undefined;\n return {\n ok: false,\n stale: true,\n error: result.ok ? 'The file changed since it was opened.' : messageOf(result),\n ...(hash === undefined ? {} : { hash }),\n };\n }\n if (!result.ok) return { ok: false, stale: false, error: messageOf(result) };\n return { ok: true, hash: isRecord(result.data) && typeof result.data.hash === 'string' ? result.data.hash : '' };\n}\n\nexport type DeleteResult = { ok: true } | { ok: false; error: string };\n\n/**\n * Removes the file from disk.\n *\n * The route takes only a path, with no hash to prove the reader saw what is\n * there now: a deletion discards the file whatever it holds, so a staleness\n * check would guard nothing. The confirmation in front of it is the guard.\n */\nexport async function deleteFile(sessionId: string, filePath: string): Promise<DeleteResult> {\n const result = await api.session(sessionId).remove({ query: { [PATH_QUERY_PARAM]: filePath } });\n return result.ok ? { ok: true } : { ok: false, error: messageOf(result) };\n}\n","import { grammarKeyOf, mediaKindOf } from '@agimon-ai/doompi-web-components';\n\nimport type { FileEditTool } from '../../../../../types/domain';\nimport type { FileEditsDiffHunk } from '../../../../../types/fileEditsApi';\nimport type { FilesItemView } from '../../../../../types/webFiles';\n\n/**\n * Pure view logic the file surfaces share: how a change is labelled, how a\n * file is identified, and how a set of notes becomes one message.\n *\n * None of this touches React or the DOM beyond what it is handed, so the\n * awkward parts (wording a review, sizing a gutter) are pinned by tests rather\n * than by clicking through a browser.\n */\n\n/** One review note anchored to source lines, or to a quoted rendered selection. */\nexport interface FileComment {\n id: string;\n path: string;\n relPath: string;\n startLine?: number;\n endLine?: number;\n snippet: string;\n body: string;\n}\n\n/** What a row says about the tool behind a change. */\nexport const TOOL_LABEL: Readonly<Record<FileEditTool, string>> = {\n edit: 'edit',\n write: 'write',\n bash: 'command',\n user: 'you',\n};\n\n/** Filters visible relative paths without changing the timeline's newest-first order. */\nexport function filterFileItems(items: readonly FilesItemView[], query: string): FilesItemView[] {\n const normalized = query.trim().toLocaleLowerCase();\n if (normalized.length === 0) return [...items];\n return items.filter((item) => item.relPath.toLocaleLowerCase().includes(normalized));\n}\n\n/** One header row and the files under it: the drawer's two-level grouping of a flat path list. */\nexport interface FileGroup {\n /** The shared directory the header names, or empty for files at the repository root. */\n prefix: string;\n items: FilesItemView[];\n}\n\n/** The directory part of a relative path, or empty for a file at the root. */\nfunction parentDir(relPath: string): string {\n const cut = relPath.lastIndexOf('/');\n return cut === -1 ? '' : relPath.slice(0, cut);\n}\n\n/** The longest directory two paths share, compared segment by segment rather than character by character. */\nfunction commonDir(left: string, right: string): string {\n const leftParts = left.split('/');\n const rightParts = right.split('/');\n const shared: string[] = [];\n for (let index = 0; index < Math.min(leftParts.length, rightParts.length); index += 1) {\n if (leftParts[index] !== rightParts[index]) break;\n shared.push(leftParts[index]);\n }\n return shared.join('/');\n}\n\n/**\n * Groups a flat list into one header per top-level segment, each headed by the\n * deepest directory that group shares.\n *\n * Two levels rather than a full tree: paths here run five and six segments\n * deep, so a node per segment spends more rows on directories than on the\n * files someone opened the drawer to find. The header carries the shared run\n * of directories and each row carries what is left, so a file stays one row\n * and the part that distinguishes it stays readable.\n *\n * Ordering is by path, not by recency, because a header only means anything\n * when its files sit together underneath it. The caller marks the newest\n * change instead.\n */\nexport function groupFileItems(items: readonly FilesItemView[]): FileGroup[] {\n const buckets = new Map<string, FilesItemView[]>();\n for (const item of items) {\n const cut = item.relPath.indexOf('/');\n const key = cut === -1 ? '' : item.relPath.slice(0, cut);\n const bucket = buckets.get(key);\n if (bucket === undefined) buckets.set(key, [item]);\n else bucket.push(item);\n }\n const groups: FileGroup[] = [];\n for (const [key, bucket] of buckets) {\n const sorted = [...bucket].sort((left, right) => left.relPath.localeCompare(right.relPath));\n // Root files have no directory to name, so they keep an empty header and\n // render their own paths in full.\n const prefix = key === '' ? '' : sorted.map((item) => parentDir(item.relPath)).reduce(commonDir);\n groups.push({ prefix, items: sorted });\n }\n return groups.sort((left, right) => left.prefix.localeCompare(right.prefix));\n}\n\n/** What one row shows once its group's header has already said the shared part. */\nexport function groupRowLabel(prefix: string, relPath: string): string {\n return prefix === '' ? relPath : relPath.slice(prefix.length + 1);\n}\n\n/**\n * How the preview shows a file: as the thing it is, wherever the browser can.\n *\n * One value rather than a chain of checks at the call site, because the order\n * carries the decisions. Media comes first, so a PNG the snapshot store\n * refused as binary is still a picture rather than an apology. `unavailable`\n * comes next, since a file with no readable text has nothing for the rest of\n * the list to render. Then the two documents that have a rendering of their\n * own, and then code, which is shown highlighted rather than as flat text.\n */\nexport type PreviewMode = 'media' | 'unavailable' | 'markdown' | 'html' | 'code' | 'text';\n\nexport function previewModeOf(filePath: string, unavailable: boolean): PreviewMode {\n if (mediaKindOf(filePath) !== 'download') return 'media';\n if (unavailable) return 'unavailable';\n const lower = filePath.toLowerCase();\n if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'markdown';\n if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'html';\n return grammarKeyOf(filePath) === undefined ? 'text' : 'code';\n}\n\n/** A short, stable fingerprint of a string; enough to separate two paths in an id. */\nfunction fingerprint(value: string): string {\n let hash = 2166136261;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n return (hash >>> 0).toString(36);\n}\n\n/** The transient tab id for one file; stable, so reopening focuses rather than duplicates. */\nexport function fileTabId(filePath: string): string {\n // The id reaches a URL, so anything outside the safe set becomes a dash, and\n // the path's own fingerprint keeps two similarly-named files apart.\n const slug = filePath.replaceAll(/[^a-zA-Z0-9]+/gu, '-').replace(/^-|-$/gu, '');\n return `files-file-${fingerprint(filePath)}-${slug.slice(-40)}`;\n}\n\n/** The widest line number in a diff, in characters, which sizes the gutter. */\nexport function gutterWidth(hunks: readonly FileEditsDiffHunk[]): number {\n let widest = 1;\n for (const hunk of hunks) {\n for (const row of hunk.rows) widest = Math.max(widest, String(row.line).length);\n }\n return widest;\n}\n\n/** Trims a quoted snippet to something a prompt can carry without drowning it. */\nexport function trimSnippet(snippet: string, maxLines = 20, maxChars = 2000): string {\n const lines = snippet.split('\\n');\n const clipped = lines.length > maxLines ? [...lines.slice(0, maxLines), '…'] : lines;\n const text = clipped.join('\\n');\n return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;\n}\n\n/** How a comment names the place it is about. */\nexport function commentAnchor(comment: FileComment): string {\n if (comment.startLine === undefined) return comment.relPath;\n if (comment.endLine === undefined || comment.endLine === comment.startLine) {\n return `${comment.relPath}:${comment.startLine}`;\n }\n return `${comment.relPath}:${comment.startLine}-${comment.endLine}`;\n}\n\n/**\n * The one message a review sends.\n *\n * Every note goes in a single prompt rather than one message each: N notes then\n * cost one turn, and the agent sees the whole review before it changes\n * anything, which is the difference between addressing a review and reacting to\n * its first line.\n */\nexport function buildReviewPrompt(comments: readonly FileComment[]): string {\n if (comments.length === 0) return '';\n const heading =\n comments.length === 1\n ? 'I left one review comment on a file you changed. Please address it.'\n : `I left ${comments.length} review comments on files you changed. Please address them.`;\n const body = comments\n .map((comment) => {\n const quoted = trimSnippet(comment.snippet)\n .split('\\n')\n .map((line) => `> ${line}`)\n .join('\\n');\n return `### ${commentAnchor(comment)}\\n\\n${quoted}\\n\\n${comment.body.trim()}`;\n })\n .join('\\n\\n');\n return `${heading}\\n\\n${body}`;\n}\n","import { Button, Textarea } from '@agimon-ai/doompi-web-components';\nimport { useState } from 'react';\n\nimport { trimSnippet } from '../_lib/fileView';\n\n/**\n * The box a selection raises: what was highlighted, and a note about it.\n *\n * The note is held here rather than in the session store because it belongs to\n * one open box and dies with it; only a submitted comment is worth surviving a\n * re-render, and that goes to the store.\n */\nexport interface CommentDraftProps {\n snippet: string;\n /** Absent when the selection came from a rendered preview, where lines cannot be recovered. */\n startLine?: number;\n endLine?: number;\n onSubmit: (body: string) => void;\n onCancel: () => void;\n}\n\nexport function CommentDraft({ snippet, startLine, endLine, onSubmit, onCancel }: CommentDraftProps) {\n const [body, setBody] = useState('');\n const range =\n startLine === undefined\n ? 'no line anchor: this selection came from the rendered preview'\n : endLine === undefined || endLine === startLine\n ? `line ${startLine}`\n : `lines ${startLine} to ${endLine}`;\n\n return (\n <div data-testid=\"files-comment-draft\" className=\"flex flex-col gap-1.5 border-t border-doom-border p-2\">\n <p className=\"text-2xs text-doom-faint\">{range}</p>\n <pre className=\"max-h-24 overflow-y-auto whitespace-pre-wrap break-words rounded border border-doom-border-soft bg-doom-deep p-1.5 font-mono text-xs text-doom-dim\">\n {trimSnippet(snippet, 8, 600)}\n </pre>\n <Textarea\n data-testid=\"files-comment-body\"\n value={body}\n rows={3}\n autoFocus\n placeholder=\"what should change here?\"\n onChange={(event) => setBody(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Escape') onCancel();\n if (event.key !== 'Enter' || !(event.metaKey || event.ctrlKey)) return;\n event.preventDefault();\n if (body.trim() !== '') onSubmit(body);\n }}\n className=\"text-sm\"\n />\n <div className=\"flex items-center gap-1.5\">\n <Button\n variant=\"outline\"\n size=\"xs\"\n data-testid=\"files-comment-add\"\n disabled={body.trim() === ''}\n onClick={() => onSubmit(body)}\n className=\"text-2xs\"\n >\n add comment\n </Button>\n <Button variant=\"ghost\" size=\"xs\" data-testid=\"files-comment-cancel\" onClick={onCancel} className=\"text-2xs\">\n cancel\n </Button>\n <span className=\"text-2xs text-doom-faint\">⌘⏎ to add</span>\n </div>\n </div>\n );\n}\n","import {\n Button,\n Dialog,\n DialogBody,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@agimon-ai/doompi-web-components';\n\n/**\n * The confirmation that stands between the delete button and the unlink.\n *\n * Deleting is the one action here with nothing behind it: a manual save can be\n * undone from the file's own history, and a comment can be removed before it\n * is sent, but this session's snapshots go when the session does, so a file\n * removed by mistake is not coming back from anything the cockpit holds.\n */\nexport interface DeleteFileDialogProps {\n relPath: string;\n open: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n}\n\nexport function DeleteFileDialog({ relPath, open, onConfirm, onCancel }: DeleteFileDialogProps) {\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onCancel();\n }}\n >\n <DialogContent width=\"sm\" data-testid=\"files-delete-dialog\" aria-describedby={undefined}>\n <DialogHeader>\n <DialogTitle>delete file</DialogTitle>\n </DialogHeader>\n <DialogBody>\n <DialogDescription>\n this removes <span className=\"font-bold text-doom-hi\">{relPath}</span> from disk. the session keeps its\n record of the change, but the file itself is gone.\n </DialogDescription>\n <DialogFooter>\n <Button variant=\"outline\" size=\"md\" data-testid=\"files-delete-cancel\" autoFocus onClick={onCancel}>\n cancel\n </Button>\n <Button variant=\"danger\" size=\"md\" data-testid=\"files-delete-confirm\" onClick={onConfirm}>\n delete\n </Button>\n </DialogFooter>\n </DialogBody>\n </DialogContent>\n </Dialog>\n );\n}\n","import type { FileEditsDiffHunk } from '../../../../../types/fileEditsApi';\nimport { gutterWidth } from '../_lib/fileView';\n\n/**\n * A diff, drawn the way this cockpit draws every other one: a numbered gutter\n * and a background wash per row, so a change reads at a glance rather than by\n * hunting for leading punctuation.\n *\n * Hunks arrive apart rather than joined by an elision marker, so the gap\n * between two of them is a rule the component draws, not a line it has to\n * recognise in a string.\n */\n\nconst ROW_TONE: Readonly<Record<string, string>> = {\n '+': 'bg-doom-tint-green text-doom-green',\n '-': 'bg-doom-tint-red text-doom-red',\n ' ': 'text-doom-dim',\n};\n\nexport interface DiffViewProps {\n hunks: readonly FileEditsDiffHunk[];\n /** Marks the surface so a test and a selection handler can find it. */\n testId: string;\n /** Called with the selected text and the lines it covers. */\n onSelect?: (selection: { startLine: number; endLine: number; snippet: string }) => void;\n}\n\nexport function DiffView({ hunks, testId, onSelect }: DiffViewProps) {\n const gutter = gutterWidth(hunks);\n\n /**\n * Turns whatever the reader highlighted into a line range. The rows carry\n * their own numbers, so the range comes from the first and last row the\n * selection touches rather than from counting newlines in the rendered text.\n */\n const handleSelect = (): void => {\n if (onSelect === undefined) return;\n const selection = globalThis.getSelection?.();\n const text = selection?.toString() ?? '';\n if (text.trim() === '') return;\n const rows = [...document.querySelectorAll<HTMLElement>(`[data-diff-surface=\"${testId}\"] [data-diff-line]`)];\n const touched = rows.filter((row) => selection?.containsNode(row, true) === true);\n const numbers = touched\n .map((row) => Number(row.dataset.diffLine))\n .filter((line) => Number.isFinite(line) && line > 0);\n if (numbers.length === 0) return;\n onSelect({ startLine: Math.min(...numbers), endLine: Math.max(...numbers), snippet: text });\n };\n\n if (hunks.length === 0) {\n return (\n <p data-testid={`${testId}-empty`} className=\"px-2 py-1 text-xs text-doom-faint\">\n no lines changed\n </p>\n );\n }\n\n return (\n <div\n data-testid={testId}\n data-diff-surface={testId}\n onMouseUp={handleSelect}\n className=\"overflow-x-auto font-mono text-sm leading-normal\"\n >\n {hunks.map((hunk, position) => (\n <div key={`${hunk.start}-${position}`}>\n {position > 0 ? (\n <div className=\"flex items-center gap-2 px-2 py-0.5 text-2xs text-doom-faint\">\n <span className=\"h-px flex-1 bg-doom-border-soft\" />\n <span>⋯</span>\n <span className=\"h-px flex-1 bg-doom-border-soft\" />\n </div>\n ) : null}\n {hunk.rows.map((row, offset) => (\n <div\n key={`${row.marker}-${row.line}-${offset}`}\n data-diff-line={row.line}\n data-diff-marker={row.marker}\n className={`flex whitespace-pre ${ROW_TONE[row.marker] ?? 'text-doom-dim'}`}\n >\n <span\n aria-hidden\n className=\"shrink-0 select-none pr-2 pl-2 text-right text-doom-faint\"\n style={{ width: `${gutter + 2}ch` }}\n >\n {row.line}\n </span>\n <span className=\"shrink-0 select-none pr-1\">{row.marker}</span>\n <span className=\"min-w-0\">{row.content}</span>\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import { MediaPreview } from '@agimon-ai/doompi-web-components';\nimport { sealedTransport } from '@agimon-ai/doompi-web-security/browser';\nimport { useEffect, useState } from 'react';\n\n/** Media elements cannot issue the encrypted fetch required by remote session files. */\nexport function SessionMediaPreview({\n src,\n path,\n 'data-testid': testId,\n}: {\n src: string;\n path: string;\n 'data-testid'?: string;\n}) {\n const [loaded, setLoaded] = useState<{ source: string; url?: string; error?: string }>();\n useEffect(() => {\n if (src === '') return;\n const controller = new AbortController();\n let url: string | undefined;\n void (async () => {\n const response = await sealedTransport.fetch(src, { signal: controller.signal, cache: 'no-store' });\n if (!response.ok) throw new Error(`Media could not be loaded (${response.status}).`);\n const blob = await response.blob();\n if (controller.signal.aborted) return;\n url = URL.createObjectURL(blob);\n setLoaded({ source: src, url });\n })().catch((error: unknown) => {\n if (!controller.signal.aborted)\n setLoaded({ source: src, error: error instanceof Error ? error.message : String(error) });\n });\n return () => {\n controller.abort();\n if (url !== undefined) URL.revokeObjectURL(url);\n };\n }, [src]);\n const current = loaded?.source === src ? loaded : undefined;\n if (current?.url === undefined)\n return <p role={current?.error ? 'alert' : 'status'}>{current?.error ?? 'Loading media...'}</p>;\n return <MediaPreview src={current.url} path={path} data-testid={testId} />;\n}\n","import type { TransientTab, WebPluginSlotProps } from '@agimon-ai/doompi-core/web';\nimport {\n Badge,\n Breadcrumb,\n Button,\n CodeEditor,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n type EditorSelectionRange,\n KebabIcon,\n Markdown,\n} from '@agimon-ai/doompi-web-components';\nimport { useStore } from '@tanstack/react-store';\nimport { useEffect, useRef, useState } from 'react';\n\nimport type { FileEditsVersionView } from '../../../../../types/fileEditsApi';\nimport { deleteFile, fetchFileDetail, saveFileContent, sessionFileUrl } from '../_lib/filesApi';\nimport {\n addComment,\n clearComments,\n files,\n markLoading,\n removeComment,\n storeDetail,\n storeError,\n} from '../_lib/filesStore';\nimport { buildReviewPrompt, commentAnchor, fileTabId, previewModeOf, TOOL_LABEL } from '../_lib/fileView';\nimport { CommentDraft } from './CommentDraft';\nimport { DeleteFileDialog } from './DeleteFileDialog';\nimport { DiffView } from './DiffView';\nimport { SessionMediaPreview as MediaPreview } from './SessionMediaPreview';\n\n/**\n * One file's tab: what this session did to it, and what the reader wants done\n * next.\n *\n * Three views over the same file. The diff answers \"what changed\", the history\n * below it answers \"in what order\", and edit is where a reader fixes a line\n * themselves rather than asking for it. Preview exists because a changed\n * README is read, not diffed, and because a changed PDF or screenshot has no\n * text to read at all.\n *\n * The tab is transient: it belongs to the session and the page, and closing it\n * is how it ends. Its cached detail lives in the session store, so reopening\n * the same file does not refetch a history the session already answered.\n */\n\n/**\n * `preview` and `diff` are the two ways of reading the file and sit in the\n * header. `edit` is a mode entered from the menu and left by picking either\n * reading view; the draft survives the trip, so leaving to check a diff does\n * not throw away what was typed.\n */\ntype View = 'preview' | 'diff' | 'edit';\n\ninterface FilePanelProps extends WebPluginSlotProps {\n filePath: string;\n relPath: string;\n}\n\ninterface Draft {\n snippet: string;\n startLine?: number;\n endLine?: number;\n}\n\n/** How a version heads its row: what ran, when, and what it moved. */\nfunction versionSummary(version: FileEditsVersionView): string {\n const time = new Date(version.at).toLocaleTimeString();\n const counts = version.hunks === undefined ? '' : ` +${version.additions} -${version.removals}`;\n return `${time} · ${TOOL_LABEL[version.tool] ?? version.tool}${counts}`;\n}\n\nexport function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransientTab }: FilePanelProps) {\n const session = useStore(files.store, (state) => files.select(state, sessionId));\n const detail = session.detail[filePath];\n const error = session.errors[filePath];\n const loading = session.loading.includes(filePath);\n const comments = session.comments.filter((comment) => comment.path === filePath);\n\n // Preview first: clicking a file is a request to look at the file, and the\n // diff is one question about it. The diff is one click away and the counts\n // beside the toggles already say whether there is one worth opening.\n const [view, setView] = useState<View>('preview');\n const [draft, setDraft] = useState<Draft | undefined>(undefined);\n const [source, setSource] = useState<string | undefined>(undefined);\n const [expanded, setExpanded] = useState<number | undefined>(undefined);\n const [saving, setSaving] = useState(false);\n const [saveNote, setSaveNote] = useState<string | undefined>(undefined);\n const [confirmingDelete, setConfirmingDelete] = useState(false);\n // The editor reports a range on every cursor move, and the comment box takes\n // focus when it opens. Holding the latest range here and raising the box on\n // mouse or key release keeps a drag from stealing focus halfway through it,\n // and a ref rather than state keeps a cursor move from re-rendering the tab.\n const selection = useRef<EditorSelectionRange | undefined>(undefined);\n\n // One fetch per open, and again whenever the session reports the file moved:\n // the count changing is the cheapest signal that the history grew.\n const revision = session.items.find((item) => item.path === filePath)?.count ?? 0;\n useEffect(() => {\n if (sessionId === null) return;\n let cancelled = false;\n markLoading(sessionId, filePath);\n void fetchFileDetail(sessionId, filePath).then((result) => {\n if (cancelled) return;\n if (result.ok) {\n storeDetail(sessionId, result.detail);\n setSource(undefined);\n setSaveNote(undefined);\n } else storeError(sessionId, filePath, result.error);\n });\n return () => {\n cancelled = true;\n };\n }, [sessionId, filePath, revision]);\n\n const working = detail?.working;\n const content = source ?? working?.content ?? '';\n const previewMode = previewModeOf(relPath, working?.unavailable === true);\n // An image, a video or a PDF is shown rather than read as text, and it is\n // shown from the hub's own bytes route rather than from the detail payload,\n // which carries text only.\n const mediaSrc = sessionId === null ? '' : sessionFileUrl(sessionId, relPath);\n\n const openDraft = (selection: Draft): void => {\n setDraft(selection);\n };\n\n /** Raises the comment box on whatever the editor last reported, if that was a selection. */\n const openSelectionDraft = (): void => {\n const range = selection.current;\n if (range === undefined || range.text.trim() === '') return;\n openDraft({ snippet: range.text, startLine: range.startLine, endLine: range.endLine });\n };\n\n const submitDraft = (body: string): void => {\n if (sessionId === null || draft === undefined) return;\n addComment(sessionId, {\n // The anchor plus the note's position is unique within a file, and a\n // comment never outlives the session that holds it.\n id: `${filePath}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,\n path: filePath,\n relPath,\n ...(draft.startLine === undefined ? {} : { startLine: draft.startLine }),\n ...(draft.endLine === undefined ? {} : { endLine: draft.endLine }),\n snippet: draft.snippet,\n body,\n });\n setDraft(undefined);\n };\n\n const sendReview = (): void => {\n if (sessionId === null || comments.length === 0) return;\n sendSessionFrame(sessionId, { type: 'prompt', message: buildReviewPrompt(comments) });\n clearComments(sessionId, filePath);\n };\n\n const confirmDelete = async (): Promise<void> => {\n if (sessionId === null) return;\n setConfirmingDelete(false);\n const result = await deleteFile(sessionId, filePath);\n if (!result.ok) {\n storeError(sessionId, filePath, result.error);\n return;\n }\n // The file leaves the dock on the next channel frame, and this tab is now\n // a view of something that is not there, so it goes with it.\n closeTransientTab(fileTabId(filePath));\n };\n\n const save = async (): Promise<void> => {\n if (sessionId === null || working === undefined || source === undefined) return;\n setSaving(true);\n const result = await saveFileContent(sessionId, filePath, working.hash, source);\n setSaving(false);\n if (result.ok) {\n setSaveNote('saved');\n setSource(undefined);\n markLoading(sessionId, filePath);\n const refreshed = await fetchFileDetail(sessionId, filePath);\n if (refreshed.ok) storeDetail(sessionId, refreshed.detail);\n else storeError(sessionId, filePath, refreshed.error);\n return;\n }\n setSaveNote(result.error);\n };\n\n return (\n <div data-testid=\"files-file-panel\" className=\"flex min-h-0 flex-1 flex-col\">\n <header className=\"flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2\">\n <Breadcrumb path={relPath} data-testid=\"files-breadcrumb\" className=\"min-w-0 flex-1\" />\n {/* Edit is not one of the header's two toggles, so with neither lit the\n mode needs saying; picking either toggle leaves it. */}\n {view === 'edit' ? (\n <Badge tone=\"neutral\" data-testid=\"files-editing\" className=\"shrink-0 text-2xs\">\n editing\n </Badge>\n ) : null}\n {detail === undefined ? null : (\n <span className=\"shrink-0 text-2xs text-doom-faint\">\n +{detail.cumulative.additions} -{detail.cumulative.removals}\n </span>\n )}\n {/* The two ways of looking at the file sit in the header; the things\n that act on it live behind the kebab, so a destructive one is never\n a mis-click away from a view toggle. */}\n <nav className=\"flex shrink-0 items-center gap-1\">\n {(['preview', 'diff'] as const).map((entry) => (\n <Button\n key={entry}\n variant={view === entry ? 'outline' : 'ghost'}\n size=\"xs\"\n data-testid={`files-view-${entry}`}\n onClick={() => setView(entry)}\n className=\"text-2xs\"\n >\n {entry}\n </Button>\n ))}\n </nav>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n data-testid=\"files-menu\"\n title=\"file actions\"\n className=\"shrink-0 text-doom-faint hover:text-doom-hi\"\n >\n <KebabIcon className=\"h-3 w-3\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent data-testid=\"files-menu-list\">\n <DropdownMenuItem\n data-testid=\"files-edit\"\n disabled={working?.unavailable === true}\n onSelect={() => setView('edit')}\n >\n edit\n </DropdownMenuItem>\n <DropdownMenuItem\n variant=\"destructive\"\n data-testid=\"files-delete\"\n disabled={working?.unavailable === true}\n onSelect={() => setConfirmingDelete(true)}\n >\n delete\n </DropdownMenuItem>\n <DropdownMenuItem data-testid=\"files-close\" onSelect={() => closeTransientTab(fileTabId(filePath))}>\n close\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </header>\n\n <DeleteFileDialog\n relPath={relPath}\n open={confirmingDelete}\n onConfirm={() => void confirmDelete()}\n onCancel={() => setConfirmingDelete(false)}\n />\n\n {loading && detail === undefined ? (\n <p data-testid=\"files-loading\" className=\"px-4 py-3 text-sm text-doom-faint\">\n reading this file's history…\n </p>\n ) : null}\n {error === undefined ? null : (\n <p data-testid=\"files-error\" className=\"px-4 py-3 text-sm text-doom-red\">\n {error}\n </p>\n )}\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {detail === undefined ? null : view === 'diff' ? (\n <div className=\"flex flex-col\">\n {detail.cumulative.note === undefined ? null : (\n <p data-testid=\"files-cumulative-note\" className=\"px-4 py-2 text-xs text-doom-yellow\">\n {detail.cumulative.note}\n </p>\n )}\n {detail.cumulative.hunks === undefined ? null : (\n <DiffView\n hunks={detail.cumulative.hunks}\n testId=\"files-cumulative-diff\"\n onSelect={(selection) => openDraft(selection)}\n />\n )}\n <section className=\"mt-2 border-t border-doom-border\">\n <h2 className=\"px-4 py-2 text-xs font-bold tracking-wide text-doom-faint uppercase\">\n {detail.versions.length} {detail.versions.length === 1 ? 'change' : 'changes'} this session\n </h2>\n {detail.versions.map((version) => (\n <article key={version.index} data-testid={`files-version-${version.index}`}>\n <Button\n variant=\"ghost\"\n size=\"card\"\n data-testid={`files-version-toggle-${version.index}`}\n onClick={() => setExpanded(expanded === version.index ? undefined : version.index)}\n className=\"w-full gap-2 rounded-none px-4 py-1.5 hover:bg-doom-panel\"\n >\n <span className=\"flex w-full min-w-0 items-center gap-2\">\n <span className=\"shrink-0 text-2xs text-doom-faint\">#{version.index}</span>\n <span className=\"min-w-0 flex-1 truncate text-left text-xs text-doom-text\">\n {versionSummary(version)}\n </span>\n {version.origin === 'scan' ? (\n <Badge tone=\"neutral\" className=\"shrink-0 text-2xs\">\n no baseline\n </Badge>\n ) : null}\n <span className=\"shrink-0 text-2xs text-doom-faint\">\n {expanded === version.index ? '▾' : '▸'}\n </span>\n </span>\n </Button>\n {expanded !== version.index ? null : version.hunks === undefined ? (\n <p className=\"px-4 py-1.5 text-xs text-doom-faint\">{version.note ?? 'nothing to show'}</p>\n ) : (\n <DiffView\n hunks={version.hunks}\n testId={`files-version-diff-${version.index}`}\n onSelect={(selection) => openDraft(selection)}\n />\n )}\n </article>\n ))}\n </section>\n </div>\n ) : view === 'edit' ? (\n <div className=\"flex h-full flex-col gap-2 p-3\">\n {working?.unavailable === true ? (\n <p data-testid=\"files-source-unavailable\" className=\"text-sm text-doom-faint\">\n {working.reason ?? 'this file cannot be shown'}\n </p>\n ) : (\n <>\n {/* The release, not the selection, is what raises the comment\n box: the editor reports a range continuously through a drag\n and the box takes focus when it opens. */}\n <div\n className=\"min-h-[24rem] flex-1\"\n onMouseUp={openSelectionDraft}\n onKeyUp={(event) => {\n if (event.shiftKey) openSelectionDraft();\n }}\n >\n <CodeEditor\n data-testid=\"files-source\"\n value={content}\n path={relPath}\n onChange={setSource}\n onSelect={(range) => {\n selection.current = range;\n }}\n className=\"h-full rounded border border-doom-border\"\n />\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"xs\"\n data-testid=\"files-save\"\n loading={saving}\n disabled={source === undefined || saving}\n onClick={() => void save()}\n className=\"text-2xs\"\n >\n save to disk\n </Button>\n {source === undefined ? null : (\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-revert\"\n onClick={() => setSource(undefined)}\n className=\"text-2xs\"\n >\n discard edits\n </Button>\n )}\n {saveNote === undefined ? null : (\n <span\n data-testid=\"files-save-note\"\n className={saveNote === 'saved' ? 'text-2xs text-doom-green' : 'text-2xs text-doom-red'}\n >\n {saveNote}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n ) : (\n <div\n data-testid=\"files-preview\"\n data-mode={previewMode}\n className={\n previewMode === 'code'\n ? 'flex h-full flex-col gap-2 p-4 text-sm text-doom-text'\n : 'flex flex-col gap-2 p-4 text-sm text-doom-text'\n }\n onMouseUp={() => {\n // Highlighted source knows which lines were picked, so a note on\n // it is anchored. A rendered document has no honest mapping back\n // to source lines, and a note there carries its quotation and\n // says it has none.\n if (previewMode === 'code') {\n openSelectionDraft();\n return;\n }\n const text = globalThis.getSelection?.()?.toString() ?? '';\n if (text.trim() !== '') openDraft({ snippet: text });\n }}\n >\n {/* A file the editor cannot hold is still a file the reader came\n to see, so the reason is said and the bytes are shown anyway. */}\n {previewMode === 'unavailable' ? (\n <p data-testid=\"files-preview-unavailable\" className=\"text-sm text-doom-faint\">\n {working?.reason ?? 'this file cannot be shown'}\n </p>\n ) : null}\n {previewMode === 'media' || previewMode === 'unavailable' ? (\n <MediaPreview src={mediaSrc} path={relPath} data-testid=\"files-preview-media\" />\n ) : previewMode === 'markdown' ? (\n <Markdown text={content} />\n ) : previewMode === 'html' ? (\n <iframe\n data-testid=\"files-preview-html\"\n title={relPath}\n sandbox=\"\"\n srcDoc={content}\n className=\"h-[36rem] w-full rounded border border-doom-border bg-doom-deep\"\n />\n ) : previewMode === 'code' ? (\n // The same editor the edit view uses, held open. Reading code as\n // flat text is the one thing a cockpit should never make anyone\n // do, and read-only keeps the two views honestly apart.\n <CodeEditor\n data-testid=\"files-preview-code\"\n value={content}\n path={relPath}\n readOnly\n onSelect={(range) => {\n selection.current = range;\n }}\n className=\"min-h-[24rem] flex-1 rounded border border-doom-border\"\n />\n ) : (\n // Everything else is shown as it is written. Preview is the file\n // as it stands, so a file with nothing to render still has a\n // reading view rather than an empty pane.\n <pre\n data-testid=\"files-preview-text\"\n className=\"whitespace-pre-wrap break-words font-mono text-sm leading-normal text-doom-text\"\n >\n {content}\n </pre>\n )}\n </div>\n )}\n </div>\n\n {draft === undefined ? null : (\n <CommentDraft\n snippet={draft.snippet}\n {...(draft.startLine === undefined ? {} : { startLine: draft.startLine })}\n {...(draft.endLine === undefined ? {} : { endLine: draft.endLine })}\n onSubmit={submitDraft}\n onCancel={() => setDraft(undefined)}\n />\n )}\n\n {comments.length === 0 ? null : (\n <footer data-testid=\"files-review\" className=\"shrink-0 border-t border-doom-border px-3 py-2\">\n <div className=\"flex items-center gap-2 pb-1\">\n <span className=\"flex-1 text-xs font-bold text-doom-hi\">\n {comments.length} {comments.length === 1 ? 'comment' : 'comments'}\n </span>\n <Button\n variant=\"outline\"\n size=\"xs\"\n data-testid=\"files-send-review\"\n onClick={sendReview}\n className=\"text-2xs\"\n >\n send review\n </Button>\n </div>\n {comments.map((comment) => (\n <div key={comment.id} data-testid=\"files-review-comment\" className=\"flex items-start gap-2 py-0.5\">\n <span className=\"shrink-0 text-2xs text-doom-faint\">{commentAnchor(comment)}</span>\n <span className=\"min-w-0 flex-1 truncate text-xs text-doom-text\">{comment.body}</span>\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-review-remove\"\n onClick={() => {\n if (sessionId !== null) removeComment(sessionId, comment.id);\n }}\n className=\"shrink-0 text-2xs\"\n >\n remove\n </Button>\n </div>\n ))}\n </footer>\n )}\n </div>\n );\n}\n\n/** The transient tab one file opens in; the same file reopens rather than duplicating. */\nexport function fileTab(filePath: string, relPath: string): TransientTab {\n return {\n id: fileTabId(filePath),\n label: relPath.split('/').at(-1) ?? relPath,\n panel: (props) => <FilePanel {...props} filePath={filePath} relPath={relPath} />,\n };\n}\n","import type { TransientTab, WebPluginSlotProps } from '@agimon-ai/doompi-core/web';\nimport { Breadcrumb, Button, CodeEditor, Markdown } from '@agimon-ai/doompi-web-components';\nimport { useEffect, useState } from 'react';\n\nimport type { FileEditsPreviewView } from '../../../../../../types/fileEditsApi';\nimport { SessionMediaPreview as MediaPreview } from '../../_components/SessionMediaPreview';\nimport { fetchFilePreview, sessionFileUrl } from '../../_lib/filesApi';\nimport { fileTabId, previewModeOf } from '../../_lib/fileView';\n\n/**\n * A file the session never changed, opened read-only.\n *\n * The tab a changed file opens is built around its history: a diff, a version\n * list, an editor, a delete. None of that exists for a file the session only\n * read, and offering an edit view over a file this package holds no baseline\n * for would put a save behind a reader who cannot see what they are about to\n * overwrite. So this is one view, the file as it stands, and nothing else.\n *\n * Its content comes from the preview route, which is bounded by the session's\n * working directory rather than by the timeline.\n */\n\ninterface FilePreviewPanelProps extends WebPluginSlotProps {\n filePath: string;\n}\n\n/** What one fetch settled on, tagged with the file it was about. */\ninterface Loaded {\n path: string;\n preview?: FileEditsPreviewView;\n error?: string;\n}\n\nexport function FilePreviewPanel({ filePath, sessionId, closeTransientTab }: FilePreviewPanelProps) {\n // The answer carries the path it answered for, so switching files is a state\n // that does not match rather than a reset the effect has to perform.\n const [loaded, setLoaded] = useState<Loaded | undefined>(undefined);\n\n useEffect(() => {\n if (sessionId === null) return;\n let cancelled = false;\n void fetchFilePreview(sessionId, filePath).then((result) => {\n if (cancelled) return;\n setLoaded(result.ok ? { path: filePath, preview: result.preview } : { path: filePath, error: result.error });\n });\n return () => {\n cancelled = true;\n };\n }, [sessionId, filePath]);\n\n const current = loaded?.path === filePath ? loaded : undefined;\n const preview = current?.preview;\n const error = current?.error;\n // Before the answer arrives the path is all there is to name the file by,\n // and the route's own relative path replaces it once it does.\n const relPath = preview?.relPath ?? filePath;\n const working = preview?.working;\n const content = working?.content ?? '';\n const previewMode = previewModeOf(relPath, working?.unavailable === true);\n const mediaSrc = sessionId === null ? '' : sessionFileUrl(sessionId, relPath);\n\n return (\n <div data-testid=\"files-preview-panel\" className=\"flex min-h-0 flex-1 flex-col\">\n <header className=\"flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2\">\n <Breadcrumb path={relPath} data-testid=\"files-preview-breadcrumb\" className=\"min-w-0 flex-1\" />\n <span className=\"shrink-0 text-2xs text-doom-faint\">unchanged</span>\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-preview-close\"\n onClick={() => closeTransientTab(filePreviewTabId(filePath))}\n className=\"shrink-0 text-2xs\"\n >\n close\n </Button>\n </header>\n\n {error === undefined ? null : (\n <p data-testid=\"files-preview-error\" className=\"px-4 py-3 text-sm text-doom-red\">\n {error}\n </p>\n )}\n {preview === undefined && error === undefined ? (\n <p data-testid=\"files-preview-loading\" className=\"px-4 py-3 text-sm text-doom-faint\">\n reading this file…\n </p>\n ) : null}\n\n {preview === undefined ? null : (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n <div\n data-testid=\"files-preview\"\n data-mode={previewMode}\n className=\"flex h-full flex-col gap-2 p-4 text-sm text-doom-text\"\n >\n {previewMode === 'unavailable' ? (\n <p data-testid=\"files-preview-unavailable\" className=\"text-sm text-doom-faint\">\n {working?.reason ?? 'this file cannot be shown'}\n </p>\n ) : null}\n {previewMode === 'media' || previewMode === 'unavailable' ? (\n <MediaPreview src={mediaSrc} path={relPath} data-testid=\"files-preview-media\" />\n ) : previewMode === 'markdown' ? (\n <Markdown text={content} />\n ) : previewMode === 'html' ? (\n <iframe\n data-testid=\"files-preview-html\"\n title={relPath}\n sandbox=\"\"\n srcDoc={content}\n className=\"h-[36rem] w-full rounded border border-doom-border bg-doom-deep\"\n />\n ) : previewMode === 'code' ? (\n <CodeEditor\n data-testid=\"files-preview-code\"\n value={content}\n path={relPath}\n readOnly\n className=\"min-h-[24rem] flex-1 rounded border border-doom-border\"\n />\n ) : (\n <pre\n data-testid=\"files-preview-text\"\n className=\"whitespace-pre-wrap break-words font-mono text-sm leading-normal text-doom-text\"\n >\n {content}\n </pre>\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\n/**\n * The id of the read-only tab, kept apart from the changed-file tab's own id:\n * the same file can be read now and changed later, and the two tabs are not\n * the same view of it.\n */\nexport function filePreviewTabId(filePath: string): string {\n return `${fileTabId(filePath)}-preview`;\n}\n\n/** The transient tab an unchanged file opens in; the same file reopens rather than duplicating. */\nexport function filePreviewTab(filePath: string): TransientTab {\n return {\n id: filePreviewTabId(filePath),\n label: filePath.split('/').at(-1) ?? filePath,\n panel: (props) => <FilePreviewPanel {...props} filePath={filePath} />,\n };\n}\n","import type { FileLinkSource, TransientTab } from '@agimon-ai/doompi-core/web';\n\nimport type { FilesItemView } from '../../../../../../types/webFiles';\nimport { fileTab } from '../../_components/FilePanel';\nimport { files } from '../../_lib/filesStore';\nimport { filePreviewTab } from '../_components/FilePreviewPanel';\n\n/**\n * The files a message names, as links into the same tab the activity dock\n * opens.\n *\n * Only files this session recorded a change to are claimed. A message quotes a\n * great deal that is shaped like nothing in particular, class names and globs\n * and flags among it, and guessing at what is a path produces links that open\n * an error. The recorded set is the one thing the page knows for certain, and\n * it is also the set a reader is most likely to want open.\n */\n\n/** A reference the way a message writes one: 'src/app.ts:42' or 'src/app.ts:42:9'. */\nconst LINE_SUFFIX = /:\\d+(?::\\d+)?$/;\n\nfunction itemsFor(sessionId: string | null): readonly FilesItemView[] {\n return files.select(files.store.state, sessionId).items;\n}\n\nfunction match(sessionId: string | null, path: string): FilesItemView | undefined {\n const candidate = path.trim().replace(LINE_SUFFIX, '');\n if (candidate === '') return undefined;\n return itemsFor(sessionId).find((item) => item.relPath === candidate || item.path === candidate);\n}\n\nexport const fileLinks: FileLinkSource = {\n subscribe(listener: () => void) {\n const subscription = files.store.subscribe(listener);\n return () => subscription.unsubscribe();\n },\n // The paths themselves, not their count: a file that leaves the list while\n // another arrives changes what resolves without changing how many do.\n fingerprint: (sessionId) =>\n itemsFor(sessionId)\n .map((item) => item.relPath)\n .join('\\n'),\n resolve: (sessionId, path): TransientTab | undefined => {\n const item = match(sessionId, path);\n return item === undefined ? undefined : fileTab(item.path, item.relPath);\n },\n // A tool argument is a path on purpose, so nothing here has to be guessed.\n // A file the session changed opens on its history; one it only read opens\n // read-only, and the route behind that tab is what decides whether a path\n // outside the working directory may be shown at all.\n openPath: (sessionId, path): TransientTab | undefined => {\n const item = match(sessionId, path);\n if (item !== undefined) return fileTab(item.path, item.relPath);\n // A call with no path argument at all hands this an empty string, and\n // there is no file to open behind it.\n const candidate = path.trim();\n return candidate === '' ? undefined : filePreviewTab(candidate);\n },\n};\n","import { defineFileLinks } from '@agimon-ai/doompi-core/web';\n\nimport { fileLinks } from './_lib/fileLinks';\nexport default defineFileLinks(fileLinks);\n","import { Button, CloseIcon, Input } from '@agimon-ai/doompi-web-components';\nimport type { ChangeEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';\nimport { Fragment, useEffect, useRef, useState } from 'react';\n\nimport type { FilesItemView } from '../../../../../../types/webFiles';\nimport { filterFileItems, groupFileItems, groupRowLabel, TOOL_LABEL } from '../../_lib/fileView';\n\nfunction FileMetadata({ item, label, recent = false }: { item: FilesItemView; label?: string; recent?: boolean }) {\n return (\n <span className=\"flex min-w-0 w-full items-center gap-1.5\">\n {/* The dock passes no label and keeps the whole path; the drawer's tree\n passes what its group header has not already said. */}\n <span className=\"min-w-0 flex-1 truncate text-left text-xs font-bold text-doom-hi\">{label ?? item.relPath}</span>\n {recent ? (\n <span data-recent=\"true\" title=\"most recent change\" className=\"shrink-0 text-2xs text-doom-blue\">\n ●\n </span>\n ) : null}\n {item.count > 1 ? <span className=\"shrink-0 text-2xs text-doom-faint\">{item.count}×</span> : null}\n <span className={`shrink-0 text-2xs ${item.diffable ? 'text-doom-faint' : 'text-doom-yellow'}`}>\n {TOOL_LABEL[item.tool] ?? item.tool}\n </span>\n </span>\n );\n}\n\nexport function FileActivityRow({ item, onOpen }: { item: FilesItemView; onOpen: () => void }) {\n return (\n <Button\n variant=\"ghost\"\n size=\"card\"\n data-testid={`activity-file-${item.relPath}`}\n data-file-diffable={item.diffable}\n title={item.diffable ? item.path : `${item.path} (changed by a command, so no diff was captured)`}\n onClick={onOpen}\n className=\"min-w-0 gap-0.5 rounded-md px-1 py-1 hover:bg-doom-panel\"\n >\n <FileMetadata item={item} />\n </Button>\n );\n}\n\n/**\n * The directory a run of rows shares.\n *\n * Deliberately not an option: the cursor counts files, and a header is a label\n * rather than something to open. Keeping it out of the listbox is what lets\n * arrow keys and Enter stay exactly what they were before the tree existed.\n */\nfunction FileGroupHeader({ prefix }: { prefix: string }) {\n return (\n <div\n data-testid={`files-browser-group-${prefix}`}\n className=\"truncate px-4 pt-2 pb-1 text-2xs font-bold tracking-wide text-doom-faint\"\n >\n {prefix}\n </div>\n );\n}\n\nfunction BrowserFileRow({\n item,\n label,\n indented = false,\n recent = false,\n selected,\n onSelect,\n onOpen,\n}: {\n item: FilesItemView;\n label: string;\n indented?: boolean;\n recent?: boolean;\n selected: boolean;\n onSelect: () => void;\n onOpen: () => void;\n}) {\n return (\n <div\n role=\"option\"\n aria-selected={selected}\n tabIndex={-1}\n data-testid={`files-browser-file-${item.relPath}`}\n data-file-diffable={item.diffable}\n data-selected={selected}\n title={item.diffable ? item.path : `${item.path} (changed by a command, so no diff was captured)`}\n onMouseEnter={onSelect}\n onClick={onOpen}\n className={`flex min-w-0 cursor-pointer border-l-2 py-2 ${indented ? 'pr-4 pl-8' : 'px-4'} ${\n selected ? 'border-doom-blue bg-doom-panel' : 'border-transparent hover:bg-doom-panel/60'\n }`}\n >\n <FileMetadata item={item} label={label} recent={recent} />\n </div>\n );\n}\n\n/** The complete session file picker, ordered exactly as the file timeline supplies it. */\nexport function FilesBrowser({\n items,\n onClose,\n onOpen,\n}: {\n items: readonly FilesItemView[];\n onClose: () => void;\n onOpen: (item: FilesItemView) => void;\n}) {\n const [query, setQuery] = useState('');\n const [selectedIndex, setSelectedIndex] = useState(0);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const shown = filterFileItems(items, query);\n // Grouping only earns its headers with the whole list in view. While a query\n // is active the reader is pinpointing one path, so the flat list answers that\n // better than a shape wrapped around two matches.\n const grouped = query.trim() === '' ? groupFileItems(shown) : undefined;\n // The order rows actually appear in, which is what the cursor counts through.\n const ordered = grouped === undefined ? shown : grouped.flatMap((group) => group.items);\n const positions = new Map(ordered.map((item, index) => [item.path, index]));\n const selected = ordered[selectedIndex];\n // The timeline hands the newest change first, so the head of the unfiltered\n // list is the one row worth marking once the tree has reordered by path.\n const recentPath = items[0]?.path;\n\n // A shrinking filter can strand the cursor past the end, so the clamp happens\n // while rendering the shorter list rather than in a pass after it paints.\n const [lastShownLength, setLastShownLength] = useState(shown.length);\n if (lastShownLength !== shown.length) {\n setLastShownLength(shown.length);\n setSelectedIndex((current) => Math.min(current, Math.max(0, shown.length - 1)));\n }\n\n useEffect(() => {\n listRef.current?.querySelector('[aria-selected=\"true\"]')?.scrollIntoView({ block: 'nearest' });\n }, [selectedIndex]);\n\n const move = (delta: number): void => {\n if (ordered.length === 0) return;\n setSelectedIndex((current) => (current + delta + ordered.length) % ordered.length);\n };\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLElement>): void => {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n move(event.key === 'ArrowDown' ? 1 : -1);\n return;\n }\n if (event.key === 'Escape') {\n event.preventDefault();\n onClose();\n return;\n }\n if (\n event.key === 'Enter' &&\n selected !== undefined &&\n (!(event.target instanceof Element) || event.target.closest('button') === null)\n ) {\n event.preventDefault();\n onOpen(selected);\n }\n };\n\n return (\n <aside\n data-testid=\"files-browser\"\n aria-label=\"changed files\"\n tabIndex={-1}\n onKeyDown={onKeyDown}\n className=\"fixed inset-y-0 right-0 z-50 flex w-[min(440px,calc(100vw-24px))] flex-col overflow-hidden border-l border-doom-border bg-doom-rail outline-none\"\n >\n <div className=\"flex h-11 shrink-0 items-center gap-2.5 border-b border-doom-border px-4\">\n <span className=\"text-base font-bold text-doom-hi\"># files</span>\n <span data-testid=\"files-browser-total\" className=\"text-2xs text-doom-faint\">\n {items.length} changed\n </span>\n <span className=\"min-w-0 flex-1\" />\n <Button\n variant=\"ghost\"\n size=\"icon\"\n data-testid=\"files-browser-close\"\n aria-label=\"close changed files\"\n title=\"close changed files\"\n onClick={onClose}\n >\n <CloseIcon className=\"h-3 w-3\" />\n </Button>\n </div>\n <div className=\"flex items-center gap-2 px-4 pt-2.5 pb-1.5\">\n <Input\n ref={inputRef}\n data-testid=\"files-browser-search\"\n aria-label=\"search changed file paths\"\n value={query}\n placeholder=\"search paths…\"\n autoFocus\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setQuery(event.target.value);\n setSelectedIndex(0);\n }}\n />\n {query.length > 0 ? (\n <Button\n variant=\"ghost\"\n size=\"xs\"\n data-testid=\"files-browser-clear\"\n aria-label=\"clear path search\"\n onClick={() => {\n setQuery('');\n setSelectedIndex(0);\n inputRef.current?.focus();\n }}\n >\n clear\n </Button>\n ) : null}\n </div>\n <p data-testid=\"files-browser-matches\" className=\"shrink-0 px-4 pb-2 text-2xs text-doom-faint\">\n {shown.length} matches · {grouped === undefined ? 'newest change first' : 'by path'}\n </p>\n <div\n ref={listRef}\n role=\"listbox\"\n aria-label=\"changed files\"\n className=\"flex min-h-0 flex-1 flex-col overflow-y-auto pb-2\"\n >\n {shown.length === 0 ? <p className=\"px-4 py-3 text-xs text-doom-faint\">nothing matches the path</p> : null}\n {grouped === undefined\n ? shown.map((item) => (\n <BrowserFileRow\n key={item.path}\n item={item}\n label={item.relPath}\n recent={item.path === recentPath}\n selected={positions.get(item.path) === selectedIndex}\n onSelect={() => setSelectedIndex(positions.get(item.path) ?? 0)}\n onOpen={() => onOpen(item)}\n />\n ))\n : grouped.map((group) => (\n <Fragment key={group.prefix}>\n {group.prefix === '' ? null : <FileGroupHeader prefix={group.prefix} />}\n {group.items.map((item) => (\n <BrowserFileRow\n key={item.path}\n item={item}\n label={groupRowLabel(group.prefix, item.relPath)}\n indented\n recent={item.path === recentPath}\n selected={positions.get(item.path) === selectedIndex}\n onSelect={() => setSelectedIndex(positions.get(item.path) ?? 0)}\n onOpen={() => onOpen(item)}\n />\n ))}\n </Fragment>\n ))}\n </div>\n <div className=\"flex h-8 shrink-0 items-center border-t border-doom-border-soft bg-doom-deep px-4\">\n <span className=\"text-2xs text-doom-faint\">↑↓ choose · enter open · esc close</span>\n </div>\n </aside>\n );\n}\n","import type { WebPluginSlotProps } from '@agimon-ai/doompi-core/web';\nimport { Button } from '@agimon-ai/doompi-web-components';\nimport { useStore } from '@tanstack/react-store';\nimport { useState } from 'react';\n\nimport type { FilesItemView } from '../../../../../../types/webFiles';\nimport { fileTab } from '../../_components/FilePanel';\nimport { files } from '../../_lib/filesStore';\nimport { FileActivityRow, FilesBrowser } from './FilesBrowser';\n\n/**\n * The files group's body in the activity dock: what this session has changed.\n *\n * Every recorded file is listed, not only the ones the agent named. A file\n * found by comparing the tree around a bash call is marked, because it is\n * openable and editable but has no baseline to diff against, and a row that\n * hid that distinction would promise a diff the tab cannot show.\n */\n\n/** The most recent changes first, which is the order the timeline already folds to. */\nconst VISIBLE_ROWS = 5;\n\nexport function FilesActivitySection({ sessionId, openTransientTab }: WebPluginSlotProps) {\n const { items } = useStore(files.store, (state) => files.select(state, sessionId));\n const [browserOpen, setBrowserOpen] = useState(false);\n\n const openFile = (item: FilesItemView): void => {\n if (sessionId === null) return;\n openTransientTab(fileTab(item.path, item.relPath));\n setBrowserOpen(false);\n };\n if (items.length === 0) {\n return (\n <p data-testid=\"activity-summary-files\" className=\"px-1 text-xs text-doom-faint\">\n nothing changed yet\n </p>\n );\n }\n\n const visible = items.slice(0, VISIBLE_ROWS);\n\n return (\n <div data-testid=\"activity-file-edits\" className=\"flex flex-col gap-0.5\">\n {visible.map((item: FilesItemView) => (\n <FileActivityRow key={item.path} item={item} onOpen={() => openFile(item)} />\n ))}\n {items.length > visible.length ? (\n <Button\n variant=\"link\"\n size=\"xs\"\n data-testid=\"activity-files-show-all\"\n aria-label={`show all ${items.length} changed files`}\n className=\"self-start px-1 pt-0.5 text-2xs text-doom-faint\"\n onClick={() => setBrowserOpen(true)}\n >\n show all {items.length} files\n </Button>\n ) : null}\n {browserOpen ? <FilesBrowser items={items} onClose={() => setBrowserOpen(false)} onOpen={openFile} /> : null}\n </div>\n );\n}\n","import { defineFill } from '@agimon-ai/doompi-core/web';\n\nimport { FilesActivitySection } from './_components/FilesActivitySection';\nexport default defineFill({ component: FilesActivitySection });\n","import { defineLeaderBinding } from '@agimon-ai/doompi-core/web';\nconst EXTENSION_GROUP = { key: 'e', label: 'extension', detail: 'tools, skills and config' };\nexport default defineLeaderBinding({\n path: [EXTENSION_GROUP, { key: 'f', label: 'files', detail: 'files this session changed' }],\n command: 'file-edits',\n});\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineWebPlugin, type WebPluginContributions } from '@agimon-ai/doompi-core/web';\n\nimport activityGroupFiles from '../src/extensions/workspaces/sessions/(frontend)/activity-group/files.web';\nimport channelFileEdits from '../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.web';\nimport fileLinksFiles from '../src/extensions/workspaces/sessions/(frontend)/file-links/files.web';\nimport fillFiles from '../src/extensions/workspaces/sessions/(frontend)/fill/Files.activity.files.web';\nimport leaderFiles from '../src/extensions/workspaces/sessions/(frontend)/leader/files.web';\n\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const webPlugin = defineWebPlugin({\n id: 'files',\n session: {\n get activityGroups(): WebPluginContributions['activityGroups'] { return [via({ name: 'files' }, activityGroupFiles)]; },\n get channels(): WebPluginContributions['channels'] { return [via({ channel: 'file_edits' }, channelFileEdits)]; },\n get fileLinks(): WebPluginContributions['fileLinks'] { return fileLinksFiles; },\n get fills(): WebPluginContributions['fills'] { return [via({ slot: 'activity.files', id: 'files' }, fillFiles)]; },\n get leaderBindings(): WebPluginContributions['leaderBindings'] { return [via({ id: 'files.files' }, leaderFiles)]; },\n },\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-file-edit",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.85",
|
|
4
4
|
"description": "Pi extension for session-scoped file-change timelines and external editor workflows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -65,12 +65,12 @@
|
|
|
65
65
|
"registry": "https://registry.npmjs.org/"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@agimon-ai/doompi-config": "0.0.1-alpha.
|
|
69
|
-
"@agimon-ai/doompi-core": "0.0.1-alpha.
|
|
70
|
-
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.
|
|
71
|
-
"@agimon-ai/doompi-ui": "0.0.1-alpha.
|
|
72
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.
|
|
73
|
-
"@agimon-ai/doompi-web-security": "0.0.1-alpha.
|
|
68
|
+
"@agimon-ai/doompi-config": "0.0.1-alpha.84",
|
|
69
|
+
"@agimon-ai/doompi-core": "0.0.1-alpha.85",
|
|
70
|
+
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.80",
|
|
71
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.85",
|
|
72
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.43",
|
|
73
|
+
"@agimon-ai/doompi-web-security": "0.0.1-alpha.43",
|
|
74
74
|
"@deepseek-ai/cordis": "4.0.2",
|
|
75
75
|
"cross-spawn": "7.0.6",
|
|
76
76
|
"hono": "4.13.8",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"typebox": "1.3.34"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@agimon-ai/doompi-build": "0.0.1-alpha.
|
|
81
|
+
"@agimon-ai/doompi-build": "0.0.1-alpha.12",
|
|
82
82
|
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
83
83
|
"@earendil-works/pi-tui": "0.87.1",
|
|
84
84
|
"@tanstack/react-store": "0.11.1",
|