@agimon-ai/doompi-file-edit 0.0.1-alpha.100 → 0.0.1-alpha.103

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.
@@ -1,5 +1,5 @@
1
1
  import { apiResponse, createApiClient, defineActivityGroup, defineApiRoutes, defineChannelFile, defineFileLinks, defineFill, defineLeaderBinding, defineSessionStore, defineWebPlugin, sessionApiAddress } from "@agimon-ai/doompi-core/web";
2
- import { Badge, Breadcrumb, Button, CloseIcon, CodeEditor, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, KebabIcon, Markdown, MediaPreview, Textarea, grammarKeyOf, mediaKindOf } from "@agimon-ai/doompi-web-components";
2
+ import { Badge, Breadcrumb, Button, CloseIcon, CodeEditor, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DiffView, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, KebabIcon, Markdown, MediaPreview, ReviewCommentDraft, buildReviewPrompt, grammarKeyOf, groupByDirectory, groupRowLabel, mediaKindOf, reviewCommentAnchor } from "@agimon-ai/doompi-web-components";
3
3
  import { useStore } from "@tanstack/react-store";
4
4
  import { Fragment, useEffect, useRef, useState } from "react";
5
5
  import { sealedTransport } from "@agimon-ai/doompi-web-security/browser";
@@ -266,6 +266,15 @@ async function deleteFile(sessionId, filePath) {
266
266
  }
267
267
  //#endregion
268
268
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/fileView.ts
269
+ /**
270
+ * Pure view logic the file surfaces share: how a change is labelled, how a
271
+ * file is identified, and how a review reaches the agent.
272
+ *
273
+ * None of this touches React or the DOM beyond what it is handed, so the
274
+ * awkward parts are pinned by tests rather than by clicking through a browser.
275
+ * Wording a review and drawing a diff are shared with other review surfaces
276
+ * and live in doompi-web-components.
277
+ */
269
278
  /** What a row says about the tool behind a change. */
270
279
  const TOOL_LABEL = {
271
280
  edit: "edit",
@@ -279,60 +288,6 @@ function filterFileItems(items, query) {
279
288
  if (normalized.length === 0) return [...items];
280
289
  return items.filter((item) => item.relPath.toLocaleLowerCase().includes(normalized));
281
290
  }
282
- /** The directory part of a relative path, or empty for a file at the root. */
283
- function parentDir(relPath) {
284
- const cut = relPath.lastIndexOf("/");
285
- return cut === -1 ? "" : relPath.slice(0, cut);
286
- }
287
- /** The longest directory two paths share, compared segment by segment rather than character by character. */
288
- function commonDir(left, right) {
289
- const leftParts = left.split("/");
290
- const rightParts = right.split("/");
291
- const shared = [];
292
- for (let index = 0; index < Math.min(leftParts.length, rightParts.length); index += 1) {
293
- if (leftParts[index] !== rightParts[index]) break;
294
- shared.push(leftParts[index]);
295
- }
296
- return shared.join("/");
297
- }
298
- /**
299
- * Groups a flat list into one header per top-level segment, each headed by the
300
- * deepest directory that group shares.
301
- *
302
- * Two levels rather than a full tree: paths here run five and six segments
303
- * deep, so a node per segment spends more rows on directories than on the
304
- * files someone opened the drawer to find. The header carries the shared run
305
- * of directories and each row carries what is left, so a file stays one row
306
- * and the part that distinguishes it stays readable.
307
- *
308
- * Ordering is by path, not by recency, because a header only means anything
309
- * when its files sit together underneath it. The caller marks the newest
310
- * change instead.
311
- */
312
- function groupFileItems(items) {
313
- const buckets = /* @__PURE__ */ new Map();
314
- for (const item of items) {
315
- const cut = item.relPath.indexOf("/");
316
- const key = cut === -1 ? "" : item.relPath.slice(0, cut);
317
- const bucket = buckets.get(key);
318
- if (bucket === void 0) buckets.set(key, [item]);
319
- else bucket.push(item);
320
- }
321
- const groups = [];
322
- for (const [key, bucket] of buckets) {
323
- const sorted = [...bucket].sort((left, right) => left.relPath.localeCompare(right.relPath));
324
- const prefix = key === "" ? "" : sorted.map((item) => parentDir(item.relPath)).reduce(commonDir);
325
- groups.push({
326
- prefix,
327
- items: sorted
328
- });
329
- }
330
- return groups.sort((left, right) => left.prefix.localeCompare(right.prefix));
331
- }
332
- /** What one row shows once its group's header has already said the shared part. */
333
- function groupRowLabel(prefix, relPath) {
334
- return prefix === "" ? relPath : relPath.slice(prefix.length + 1);
335
- }
336
291
  function previewModeOf(filePath, unavailable) {
337
292
  if (mediaKindOf(filePath) !== "download") return "media";
338
293
  if (unavailable) return "unavailable";
@@ -355,99 +310,25 @@ function fileTabId(filePath) {
355
310
  const slug = filePath.replaceAll(/[^a-zA-Z0-9]+/gu, "-").replace(/^-|-$/gu, "");
356
311
  return `files-file-${fingerprint(filePath)}-${slug.slice(-40)}`;
357
312
  }
358
- /** The widest line number in a diff, in characters, which sizes the gutter. */
359
- function gutterWidth(hunks) {
360
- let widest = 1;
361
- for (const hunk of hunks) for (const row of hunk.rows) widest = Math.max(widest, String(row.line).length);
362
- return widest;
363
- }
364
- /** Trims a quoted snippet to something a prompt can carry without drowning it. */
365
- function trimSnippet(snippet, maxLines = 20, maxChars = 2e3) {
366
- const lines = snippet.split("\n");
367
- const text = (lines.length > maxLines ? [...lines.slice(0, maxLines), "…"] : lines).join("\n");
368
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
369
- }
370
- /** How a comment names the place it is about. */
371
- function commentAnchor(comment) {
372
- if (comment.startLine === void 0) return comment.relPath;
373
- if (comment.endLine === void 0 || comment.endLine === comment.startLine) return `${comment.relPath}:${comment.startLine}`;
374
- return `${comment.relPath}:${comment.startLine}-${comment.endLine}`;
375
- }
376
313
  /**
377
- * The one message a review sends.
314
+ * Sends a review as one queued message and reports whether it left.
378
315
  *
379
- * Every note goes in a single prompt rather than one message each: N notes then
380
- * cost one turn, and the agent sees the whole review before it changes
381
- * anything, which is the difference between addressing a review and reacting to
382
- * its first line.
316
+ * `enqueue_automatic` rather than `prompt`: a prompt is refused while the agent
317
+ * is mid-turn, and the review would be gone with it. A queued message waits
318
+ * behind the running turn and starts at once when the agent is idle. The
319
+ * sender throws when the session is not connected, and that reason comes back
320
+ * so the caller keeps the comments and says why.
383
321
  */
384
- function buildReviewPrompt(comments) {
385
- if (comments.length === 0) return "";
386
- return `${comments.length === 1 ? "I left one review comment on a file you changed. Please address it." : `I left ${comments.length} review comments on files you changed. Please address them.`}\n\n${comments.map((comment) => {
387
- const quoted = trimSnippet(comment.snippet).split("\n").map((line) => `> ${line}`).join("\n");
388
- return `### ${commentAnchor(comment)}\n\n${quoted}\n\n${comment.body.trim()}`;
389
- }).join("\n\n")}`;
390
- }
391
- //#endregion
392
- //#region src/extensions/workspaces/sessions/(frontend)/_components/CommentDraft.tsx
393
- function CommentDraft({ snippet, startLine, endLine, onSubmit, onCancel }) {
394
- const [body, setBody] = useState("");
395
- const range = startLine === void 0 ? "no line anchor: this selection came from the rendered preview" : endLine === void 0 || endLine === startLine ? `line ${startLine}` : `lines ${startLine} to ${endLine}`;
396
- return /* @__PURE__ */ jsxs("div", {
397
- "data-testid": "files-comment-draft",
398
- className: "flex flex-col gap-1.5 border-t border-doom-border p-2",
399
- children: [
400
- /* @__PURE__ */ jsx("p", {
401
- className: "text-2xs text-doom-faint",
402
- children: range
403
- }),
404
- /* @__PURE__ */ jsx("pre", {
405
- 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",
406
- children: trimSnippet(snippet, 8, 600)
407
- }),
408
- /* @__PURE__ */ jsx(Textarea, {
409
- "data-testid": "files-comment-body",
410
- value: body,
411
- rows: 3,
412
- autoFocus: true,
413
- placeholder: "what should change here?",
414
- onChange: (event) => setBody(event.target.value),
415
- onKeyDown: (event) => {
416
- if (event.key === "Escape") onCancel();
417
- if (event.key !== "Enter" || !(event.metaKey || event.ctrlKey)) return;
418
- event.preventDefault();
419
- if (body.trim() !== "") onSubmit(body);
420
- },
421
- className: "text-sm"
422
- }),
423
- /* @__PURE__ */ jsxs("div", {
424
- className: "flex items-center gap-1.5",
425
- children: [
426
- /* @__PURE__ */ jsx(Button, {
427
- variant: "outline",
428
- size: "xs",
429
- "data-testid": "files-comment-add",
430
- disabled: body.trim() === "",
431
- onClick: () => onSubmit(body),
432
- className: "text-2xs",
433
- children: "add comment"
434
- }),
435
- /* @__PURE__ */ jsx(Button, {
436
- variant: "ghost",
437
- size: "xs",
438
- "data-testid": "files-comment-cancel",
439
- onClick: onCancel,
440
- className: "text-2xs",
441
- children: "cancel"
442
- }),
443
- /* @__PURE__ */ jsx("span", {
444
- className: "text-2xs text-doom-faint",
445
- children: "⌘⏎ to add"
446
- })
447
- ]
448
- })
449
- ]
450
- });
322
+ function sendReviewFrame(send, comments) {
323
+ try {
324
+ send({
325
+ type: "enqueue_automatic",
326
+ message: buildReviewPrompt(comments)
327
+ });
328
+ return;
329
+ } catch (error) {
330
+ return error instanceof Error ? error.message : String(error);
331
+ }
451
332
  }
452
333
  //#endregion
453
334
  //#region src/extensions/workspaces/sessions/(frontend)/_components/DeleteFileDialog.tsx
@@ -486,82 +367,6 @@ function DeleteFileDialog({ relPath, open, onConfirm, onCancel }) {
486
367
  });
487
368
  }
488
369
  //#endregion
489
- //#region src/extensions/workspaces/sessions/(frontend)/_components/DiffView.tsx
490
- /**
491
- * A diff, drawn the way this cockpit draws every other one: a numbered gutter
492
- * and a background wash per row, so a change reads at a glance rather than by
493
- * hunting for leading punctuation.
494
- *
495
- * Hunks arrive apart rather than joined by an elision marker, so the gap
496
- * between two of them is a rule the component draws, not a line it has to
497
- * recognise in a string.
498
- */
499
- const ROW_TONE = {
500
- "+": "bg-doom-tint-green text-doom-green",
501
- "-": "bg-doom-tint-red text-doom-red",
502
- " ": "text-doom-dim"
503
- };
504
- function DiffView({ hunks, testId, onSelect }) {
505
- const gutter = gutterWidth(hunks);
506
- /**
507
- * Turns whatever the reader highlighted into a line range. The rows carry
508
- * their own numbers, so the range comes from the first and last row the
509
- * selection touches rather than from counting newlines in the rendered text.
510
- */
511
- const handleSelect = () => {
512
- if (onSelect === void 0) return;
513
- const selection = globalThis.getSelection?.();
514
- const text = selection?.toString() ?? "";
515
- if (text.trim() === "") return;
516
- const numbers = [...document.querySelectorAll(`[data-diff-surface="${testId}"] [data-diff-line]`)].filter((row) => selection?.containsNode(row, true) === true).map((row) => Number(row.dataset.diffLine)).filter((line) => Number.isFinite(line) && line > 0);
517
- if (numbers.length === 0) return;
518
- onSelect({
519
- startLine: Math.min(...numbers),
520
- endLine: Math.max(...numbers),
521
- snippet: text
522
- });
523
- };
524
- if (hunks.length === 0) return /* @__PURE__ */ jsx("p", {
525
- "data-testid": `${testId}-empty`,
526
- className: "px-2 py-1 text-xs text-doom-faint",
527
- children: "no lines changed"
528
- });
529
- return /* @__PURE__ */ jsx("div", {
530
- "data-testid": testId,
531
- "data-diff-surface": testId,
532
- onMouseUp: handleSelect,
533
- className: "overflow-x-auto font-mono text-sm leading-normal",
534
- children: hunks.map((hunk, position) => /* @__PURE__ */ jsxs("div", { children: [position > 0 ? /* @__PURE__ */ jsxs("div", {
535
- className: "flex items-center gap-2 px-2 py-0.5 text-2xs text-doom-faint",
536
- children: [
537
- /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-doom-border-soft" }),
538
- /* @__PURE__ */ jsx("span", { children: "⋯" }),
539
- /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-doom-border-soft" })
540
- ]
541
- }) : null, hunk.rows.map((row, offset) => /* @__PURE__ */ jsxs("div", {
542
- "data-diff-line": row.line,
543
- "data-diff-marker": row.marker,
544
- className: `flex whitespace-pre ${ROW_TONE[row.marker] ?? "text-doom-dim"}`,
545
- children: [
546
- /* @__PURE__ */ jsx("span", {
547
- "aria-hidden": true,
548
- className: "shrink-0 select-none pr-2 pl-2 text-right text-doom-faint",
549
- style: { width: `${gutter + 2}ch` },
550
- children: row.line
551
- }),
552
- /* @__PURE__ */ jsx("span", {
553
- className: "shrink-0 select-none pr-1",
554
- children: row.marker
555
- }),
556
- /* @__PURE__ */ jsx("span", {
557
- className: "min-w-0",
558
- children: row.content
559
- })
560
- ]
561
- }, `${row.marker}-${row.line}-${offset}`))] }, `${hunk.start}-${position}`))
562
- });
563
- }
564
- //#endregion
565
370
  //#region src/extensions/workspaces/sessions/(frontend)/_components/SessionMediaPreview.tsx
566
371
  /** Media elements cannot issue the encrypted fetch required by remote session files. */
567
372
  function SessionMediaPreview({ src, path, "data-testid": testId }) {
@@ -626,6 +431,7 @@ function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransi
626
431
  const [saving, setSaving] = useState(false);
627
432
  const [saveNote, setSaveNote] = useState(void 0);
628
433
  const [confirmingDelete, setConfirmingDelete] = useState(false);
434
+ const [reviewError, setReviewError] = useState(void 0);
629
435
  const selection = useRef(void 0);
630
436
  const revision = session.items.find((item) => item.path === filePath)?.count ?? 0;
631
437
  useEffect(() => {
@@ -668,9 +474,10 @@ function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransi
668
474
  const submitDraft = (body) => {
669
475
  if (sessionId === null || draft === void 0) return;
670
476
  addComment(sessionId, {
671
- id: `${filePath}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,
477
+ id: `${filePath}:${draft.side ?? "new"}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,
672
478
  path: filePath,
673
479
  relPath,
480
+ ...draft.side === void 0 ? {} : { side: draft.side },
674
481
  ...draft.startLine === void 0 ? {} : { startLine: draft.startLine },
675
482
  ...draft.endLine === void 0 ? {} : { endLine: draft.endLine },
676
483
  snippet: draft.snippet,
@@ -680,11 +487,9 @@ function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransi
680
487
  };
681
488
  const sendReview = () => {
682
489
  if (sessionId === null || comments.length === 0) return;
683
- sendSessionFrame(sessionId, {
684
- type: "prompt",
685
- message: buildReviewPrompt(comments)
686
- });
687
- clearComments(sessionId, filePath);
490
+ const failure = sendReviewFrame((frame) => sendSessionFrame(sessionId, frame), comments);
491
+ setReviewError(failure);
492
+ if (failure === void 0) clearComments(sessionId, filePath);
688
493
  };
689
494
  const confirmDelete = async () => {
690
495
  if (sessionId === null) return;
@@ -959,8 +764,10 @@ function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransi
959
764
  })]
960
765
  })
961
766
  }),
962
- draft === void 0 ? null : /* @__PURE__ */ jsx(CommentDraft, {
767
+ draft === void 0 ? null : /* @__PURE__ */ jsx(ReviewCommentDraft, {
768
+ testIdPrefix: "files-comment",
963
769
  snippet: draft.snippet,
770
+ ...draft.side === void 0 ? {} : { side: draft.side },
964
771
  ...draft.startLine === void 0 ? {} : { startLine: draft.startLine },
965
772
  ...draft.endLine === void 0 ? {} : { endLine: draft.endLine },
966
773
  onSubmit: submitDraft,
@@ -969,47 +776,56 @@ function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransi
969
776
  comments.length === 0 ? null : /* @__PURE__ */ jsxs("footer", {
970
777
  "data-testid": "files-review",
971
778
  className: "shrink-0 border-t border-doom-border px-3 py-2",
972
- children: [/* @__PURE__ */ jsxs("div", {
973
- className: "flex items-center gap-2 pb-1",
974
- children: [/* @__PURE__ */ jsxs("span", {
975
- className: "flex-1 text-xs font-bold text-doom-hi",
779
+ children: [
780
+ /* @__PURE__ */ jsxs("div", {
781
+ className: "flex items-center gap-2 pb-1",
782
+ children: [/* @__PURE__ */ jsxs("span", {
783
+ className: "flex-1 text-xs font-bold text-doom-hi",
784
+ children: [
785
+ comments.length,
786
+ " ",
787
+ comments.length === 1 ? "comment" : "comments"
788
+ ]
789
+ }), /* @__PURE__ */ jsx(Button, {
790
+ variant: "outline",
791
+ size: "xs",
792
+ "data-testid": "files-send-review",
793
+ onClick: sendReview,
794
+ className: "text-2xs",
795
+ children: "send review"
796
+ })]
797
+ }),
798
+ reviewError === void 0 ? null : /* @__PURE__ */ jsxs("p", {
799
+ role: "alert",
800
+ "data-testid": "files-review-error",
801
+ className: "pb-1 text-2xs text-doom-red",
802
+ children: ["review not sent: ", reviewError]
803
+ }),
804
+ comments.map((comment) => /* @__PURE__ */ jsxs("div", {
805
+ "data-testid": "files-review-comment",
806
+ className: "flex items-start gap-2 py-0.5",
976
807
  children: [
977
- comments.length,
978
- " ",
979
- comments.length === 1 ? "comment" : "comments"
808
+ /* @__PURE__ */ jsx("span", {
809
+ className: "shrink-0 text-2xs text-doom-faint",
810
+ children: reviewCommentAnchor(comment)
811
+ }),
812
+ /* @__PURE__ */ jsx("span", {
813
+ className: "min-w-0 flex-1 truncate text-xs text-doom-text",
814
+ children: comment.body
815
+ }),
816
+ /* @__PURE__ */ jsx(Button, {
817
+ variant: "ghost",
818
+ size: "xs",
819
+ "data-testid": "files-review-remove",
820
+ onClick: () => {
821
+ if (sessionId !== null) removeComment(sessionId, comment.id);
822
+ },
823
+ className: "shrink-0 text-2xs",
824
+ children: "remove"
825
+ })
980
826
  ]
981
- }), /* @__PURE__ */ jsx(Button, {
982
- variant: "outline",
983
- size: "xs",
984
- "data-testid": "files-send-review",
985
- onClick: sendReview,
986
- className: "text-2xs",
987
- children: "send review"
988
- })]
989
- }), comments.map((comment) => /* @__PURE__ */ jsxs("div", {
990
- "data-testid": "files-review-comment",
991
- className: "flex items-start gap-2 py-0.5",
992
- children: [
993
- /* @__PURE__ */ jsx("span", {
994
- className: "shrink-0 text-2xs text-doom-faint",
995
- children: commentAnchor(comment)
996
- }),
997
- /* @__PURE__ */ jsx("span", {
998
- className: "min-w-0 flex-1 truncate text-xs text-doom-text",
999
- children: comment.body
1000
- }),
1001
- /* @__PURE__ */ jsx(Button, {
1002
- variant: "ghost",
1003
- size: "xs",
1004
- "data-testid": "files-review-remove",
1005
- onClick: () => {
1006
- if (sessionId !== null) removeComment(sessionId, comment.id);
1007
- },
1008
- className: "shrink-0 text-2xs",
1009
- children: "remove"
1010
- })
1011
- ]
1012
- }, comment.id))]
827
+ }, comment.id))
828
+ ]
1013
829
  })
1014
830
  ]
1015
831
  });
@@ -1266,7 +1082,7 @@ function FilesBrowser({ items, onClose, onOpen }) {
1266
1082
  const inputRef = useRef(null);
1267
1083
  const listRef = useRef(null);
1268
1084
  const shown = filterFileItems(items, query);
1269
- const grouped = query.trim() === "" ? groupFileItems(shown) : void 0;
1085
+ const grouped = query.trim() === "" ? groupByDirectory(shown, (item) => item.relPath) : void 0;
1270
1086
  const ordered = grouped === void 0 ? shown : grouped.flatMap((group) => group.items);
1271
1087
  const positions = new Map(ordered.map((item, index) => [item.path, index]));
1272
1088
  const selected = ordered[selectedIndex];
@@ -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 =>\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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"}
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/DeleteFileDialog.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';\nimport type { ReviewComment } from '@agimon-ai/doompi-web-components';\n\nimport type { FileEditsDetailView } from '../../../../../types/fileEditsApi';\nimport { filesChannelType, type FilesItemView } from '../../../../../types/webFiles';\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 ReviewComment[];\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: ReviewComment): 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 { buildReviewPrompt, grammarKeyOf, mediaKindOf, type ReviewComment } from '@agimon-ai/doompi-web-components';\n\nimport type { FileEditTool } from '../../../../../types/domain';\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 review reaches the agent.\n *\n * None of this touches React or the DOM beyond what it is handed, so the\n * awkward parts are pinned by tests rather than by clicking through a browser.\n * Wording a review and drawing a diff are shared with other review surfaces\n * and live in doompi-web-components.\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/**\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/**\n * Sends a review as one queued message and reports whether it left.\n *\n * `enqueue_automatic` rather than `prompt`: a prompt is refused while the agent\n * is mid-turn, and the review would be gone with it. A queued message waits\n * behind the running turn and starts at once when the agent is idle. The\n * sender throws when the session is not connected, and that reason comes back\n * so the caller keeps the comments and says why.\n */\nexport function sendReviewFrame(\n send: (frame: Record<string, unknown>) => void,\n comments: readonly ReviewComment[],\n): string | undefined {\n try {\n send({ type: 'enqueue_automatic', message: buildReviewPrompt(comments) });\n return undefined;\n } catch (error) {\n return error instanceof Error ? error.message : String(error);\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 { 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 type DiffSide,\n DiffView,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n type EditorSelectionRange,\n KebabIcon,\n Markdown,\n ReviewCommentDraft,\n reviewCommentAnchor,\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 { fileTabId, previewModeOf, sendReviewFrame, TOOL_LABEL } from '../_lib/fileView';\nimport { DeleteFileDialog } from './DeleteFileDialog';\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 /** Set by a diff selection; absent means the new side, as in the editor and preview. */\n side?: DiffSide;\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 const [reviewError, setReviewError] = useState<string | undefined>(undefined);\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.side ?? 'new'}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,\n path: filePath,\n relPath,\n ...(draft.side === undefined ? {} : { side: draft.side }),\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 // The comments go only once the frame has left; a disconnected session\n // keeps them so the reader can send again rather than retype them.\n const failure = sendReviewFrame((frame) => sendSessionFrame(sessionId, frame), comments);\n setReviewError(failure);\n if (failure === undefined) 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 <ReviewCommentDraft\n testIdPrefix=\"files-comment\"\n snippet={draft.snippet}\n {...(draft.side === undefined ? {} : { side: draft.side })}\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 {reviewError === undefined ? null : (\n <p role=\"alert\" data-testid=\"files-review-error\" className=\"pb-1 text-2xs text-doom-red\">\n review not sent: {reviewError}\n </p>\n )}\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\">{reviewCommentAnchor(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, groupByDirectory, groupRowLabel, 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, 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. Groups are ordered by path,\n // not recency, so a header's files sit together under it.\n const grouped = query.trim() === '' ? groupByDirectory(shown, (item) => item.relPath) : 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.100",
3
+ "version": "0.0.1-alpha.103",
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.99",
69
- "@agimon-ai/doompi-core": "0.0.1-alpha.100",
70
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.90",
71
- "@agimon-ai/doompi-ui": "0.0.1-alpha.100",
72
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.58",
73
- "@agimon-ai/doompi-web-security": "0.0.1-alpha.53",
68
+ "@agimon-ai/doompi-config": "0.0.1-alpha.102",
69
+ "@agimon-ai/doompi-core": "0.0.1-alpha.103",
70
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.92",
71
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.103",
72
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.61",
73
+ "@agimon-ai/doompi-web-security": "0.0.1-alpha.55",
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.22",
81
+ "@agimon-ai/doompi-build": "0.0.1-alpha.24",
82
82
  "@earendil-works/pi-coding-agent": "0.99.1",
83
83
  "@earendil-works/pi-tui": "0.99.1",
84
84
  "@tanstack/react-store": "0.11.1",