@agimon-ai/po-file-edit 0.0.0-alpha.34 → 0.0.0-alpha.35

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,9 +1,9 @@
1
- import { createApiClient, defineChannelFile, defineFileLinks, defineNativePlugin, defineSessionStore, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
1
+ import { createApiClient, defineChannelFile, defineFileLinks, defineNativePlugin, defineSessionStore, encodeBase64, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
2
2
  import { View } from "react-native-unistyles/components/native/View";
3
3
  import { Text } from "react-native-unistyles/components/native/Text";
4
4
  import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
5
5
  import { Button, CloseIcon, CodeBlock, CopyButton, DownloadIcon, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, KebabIcon, Label, Markdown, SegmentedControl, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Textarea, fontOf } from "@agimon-ai/po-native-components";
6
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
6
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
7
  import { StyleSheet } from "react-native-unistyles";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/types/webFiles.ts
@@ -294,6 +294,107 @@ async function prepareFileSave(sessionId, path, signal) {
294
294
  };
295
295
  }
296
296
  //#endregion
297
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/FileDiff.native.tsx
298
+ const ROW_STEP = 200;
299
+ /** Mount only the disclosed prefix; source line numbers and single-line comment anchors stay intact. */
300
+ function FileDiff({ hunks, comment, initialRows = ROW_STEP, label = "file-diff" }) {
301
+ const [limit, setLimit] = useState(initialRows);
302
+ const total = useMemo(() => hunks.reduce((count, hunk) => count + hunk.rows.length, 0), [hunks]);
303
+ const visible = useMemo(() => {
304
+ const result = [];
305
+ let remaining = limit;
306
+ for (const hunk of hunks) {
307
+ if (remaining <= 0) break;
308
+ result.push({
309
+ ...hunk,
310
+ rows: hunk.rows.slice(0, remaining)
311
+ });
312
+ remaining -= hunk.rows.length;
313
+ }
314
+ return result;
315
+ }, [hunks, limit]);
316
+ return /* @__PURE__ */ jsxs(View, {
317
+ style: styles$3.diff,
318
+ testID: label,
319
+ children: [visible.map((hunk, index) => {
320
+ return hunk.rows.length ? /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
321
+ style: styles$3.meta,
322
+ children: ["line ", hunk.start]
323
+ }), hunk.rows.map((row, position) => /* @__PURE__ */ jsx(Button, {
324
+ variant: "ghost",
325
+ accessibilityLabel: `comment on ${row.marker === "-" ? "removed" : "new"} line ${row.line}`,
326
+ onPress: () => comment({
327
+ snippet: row.content,
328
+ side: row.marker === "-" ? "old" : "new",
329
+ startLine: row.line,
330
+ endLine: row.line
331
+ }),
332
+ children: /* @__PURE__ */ jsxs(Text, {
333
+ selectable: true,
334
+ style: row.marker === "+" ? styles$3.added : row.marker === "-" ? styles$3.removed : styles$3.code,
335
+ children: [
336
+ row.marker,
337
+ " ",
338
+ row.line,
339
+ " ",
340
+ row.content
341
+ ]
342
+ })
343
+ }, `${row.line}-${position}`))] }, `${hunk.start}-${index}`) : null;
344
+ }), total ? /* @__PURE__ */ jsxs(View, {
345
+ style: styles$3.diff,
346
+ children: [/* @__PURE__ */ jsxs(Text, {
347
+ accessibilityLiveRegion: "polite",
348
+ style: styles$3.meta,
349
+ children: [
350
+ "Showing ",
351
+ Math.min(limit, total).toLocaleString("en-US"),
352
+ " of ",
353
+ total.toLocaleString("en-US"),
354
+ " rows"
355
+ ]
356
+ }), limit < total ? /* @__PURE__ */ jsxs(Button, {
357
+ variant: "outline",
358
+ onPress: () => setLimit((value) => Math.min(total, value + ROW_STEP)),
359
+ children: [
360
+ "Show ",
361
+ Math.min(ROW_STEP, total - limit),
362
+ " more rows"
363
+ ]
364
+ }) : null]
365
+ }) : null]
366
+ });
367
+ }
368
+ const styles$3 = StyleSheet.create((theme) => ({
369
+ diff: {
370
+ gap: theme.spacing(2),
371
+ uni__dependencies: [0]
372
+ },
373
+ code: {
374
+ fontFamily: fontOf(theme.fontFamily.mono),
375
+ fontSize: theme.fontSize.sm,
376
+ color: theme.colors.text,
377
+ uni__dependencies: [0]
378
+ },
379
+ added: {
380
+ fontFamily: fontOf(theme.fontFamily.mono),
381
+ fontSize: theme.fontSize.sm,
382
+ color: theme.colors.green,
383
+ uni__dependencies: [0]
384
+ },
385
+ removed: {
386
+ fontFamily: fontOf(theme.fontFamily.mono),
387
+ fontSize: theme.fontSize.sm,
388
+ color: theme.colors.red,
389
+ uni__dependencies: [0]
390
+ },
391
+ meta: {
392
+ fontSize: theme.fontSize.sm,
393
+ color: theme.colors.dim,
394
+ uni__dependencies: [0]
395
+ }
396
+ }));
397
+ //#endregion
297
398
  //#region src/extensions/workspaces/sessions/(frontend)/_components/FilePreview.native.tsx
298
399
  const MEDIA = /\.(?:png|jpe?g|gif|webp|bmp|svg|ico|mp3|wav|m4a|ogg|mp4|mov|webm|pdf)$/iu;
299
400
  const MAX_MEDIA_BYTES = 5242880;
@@ -301,23 +402,6 @@ const URL_OR_ANCHOR = /^(?:[a-z][a-z\d+.-]*:|\/\/|#|\?)/iu;
301
402
  function isMediaPath(path) {
302
403
  return MEDIA.test(path);
303
404
  }
304
- function base64(bytes) {
305
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
306
- const parts = [];
307
- let chunk = "";
308
- for (let offset = 0; offset < bytes.length; offset += 3) {
309
- const first = bytes[offset];
310
- const second = bytes[offset + 1];
311
- const third = bytes[offset + 2];
312
- chunk += alphabet[first >> 2] + alphabet[(first & 3) << 4 | (second ?? 0) >> 4] + (second === void 0 ? "=" : alphabet[(second & 15) << 2 | (third ?? 0) >> 6]) + (third === void 0 ? "=" : alphabet[third & 63]);
313
- if (chunk.length >= 8192) {
314
- parts.push(chunk);
315
- chunk = "";
316
- }
317
- }
318
- parts.push(chunk);
319
- return parts.join("");
320
- }
321
405
  /** One media file's bytes through the package's guarded file route, as base64 with its content type. */
322
406
  async function fetchMedia(sessionId, path, signal) {
323
407
  const result = await api.session(sessionId).file({
@@ -331,7 +415,7 @@ async function fetchMedia(sessionId, path, signal) {
331
415
  const type = result.response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
332
416
  if (!/^(?:image\/[a-z0-9.+-]+|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+|application\/pdf)$/u.test(type)) throw new Error("This media type cannot be previewed.");
333
417
  return {
334
- data: base64(bytes),
418
+ data: encodeBase64(bytes),
335
419
  type
336
420
  };
337
421
  }
@@ -382,6 +466,73 @@ function SessionMedia({ sessionId, path, renderMediaPreview }) {
382
466
  children: "This host cannot display this media preview."
383
467
  });
384
468
  }
469
+ /** One document's requested images, invalidated only by their own file revisions. */
470
+ function markdownImages(sessionId, documentPath) {
471
+ const loaded = /* @__PURE__ */ new Map();
472
+ const stamp = (imagePath) => {
473
+ const session = files.select(files.store.state, sessionId);
474
+ const item = session.items.find((entry) => entry.path === imagePath || entry.relPath === imagePath);
475
+ return JSON.stringify([
476
+ item?.count,
477
+ item?.at,
478
+ session.detail[imagePath]?.working.hash
479
+ ]);
480
+ };
481
+ const load = async (source) => {
482
+ if (sessionId === null || URL_OR_ANCHOR.test(source)) throw new Error("This image cannot be loaded.");
483
+ const imagePath = documentRelative(documentPath, source);
484
+ let image = loaded.get(imagePath);
485
+ if (!image) {
486
+ image = { stamp: stamp(imagePath) };
487
+ loaded.set(imagePath, image);
488
+ }
489
+ const cached = image;
490
+ cached.asset ??= fetchMedia(sessionId, imagePath, new AbortController().signal).then((media) => {
491
+ if (!media.type.startsWith("image/")) throw new Error("This file is not an image.");
492
+ return {
493
+ url: `data:${media.type};base64,${media.data}`,
494
+ dispose: () => {}
495
+ };
496
+ }).catch((error) => {
497
+ cached.asset = void 0;
498
+ throw error;
499
+ });
500
+ return cached.asset;
501
+ };
502
+ let snapshot = load;
503
+ const subscribe = (listener) => {
504
+ const subscription = files.store.subscribe(() => {
505
+ let changed = false;
506
+ for (const [imagePath, image] of loaded) {
507
+ const next = stamp(imagePath);
508
+ if (next === image.stamp) continue;
509
+ loaded.set(imagePath, { stamp: next });
510
+ changed = true;
511
+ }
512
+ if (changed) {
513
+ snapshot = (source) => load(source);
514
+ listener();
515
+ }
516
+ });
517
+ return () => subscription.unsubscribe();
518
+ };
519
+ return {
520
+ subscribe,
521
+ snapshot: () => snapshot
522
+ };
523
+ }
524
+ function MarkdownPreview({ path, working, sessionId, fileTabFor, openTransientTab }) {
525
+ const images = useMemo(() => markdownImages(sessionId, path), [sessionId, path]);
526
+ const loadImage = useSyncExternalStore(images.subscribe, images.snapshot);
527
+ return /* @__PURE__ */ jsx(Markdown, {
528
+ text: working.content,
529
+ onFileLink: (file) => {
530
+ const tab = fileTabFor(file);
531
+ return tab ? () => openTransientTab(tab) : void 0;
532
+ },
533
+ loadImage
534
+ });
535
+ }
385
536
  /** Rendering is separate from reading: only the guarded package API supplies file content or bytes. */
386
537
  function FilePreview({ path, working, ...props }) {
387
538
  if (MEDIA.test(path)) return /* @__PURE__ */ jsx(SessionMedia, {
@@ -404,25 +555,11 @@ function FilePreview({ path, working, ...props }) {
404
555
  style: styles$2.error,
405
556
  children: "This host cannot display HTML previews."
406
557
  });
407
- if (/\.(?:md|markdown)$/iu.test(path)) {
408
- const { sessionId } = props;
409
- return /* @__PURE__ */ jsx(Markdown, {
410
- text: working.content,
411
- onFileLink: (file) => {
412
- const tab = props.fileTabFor(file);
413
- return tab ? () => props.openTransientTab(tab) : void 0;
414
- },
415
- loadImage: async (source) => {
416
- if (sessionId === null || URL_OR_ANCHOR.test(source)) throw new Error("This image cannot be loaded.");
417
- const media = await fetchMedia(sessionId, documentRelative(path, source), new AbortController().signal);
418
- if (!media.type.startsWith("image/")) throw new Error("This file is not an image.");
419
- return {
420
- url: `data:${media.type};base64,${media.data}`,
421
- dispose: () => {}
422
- };
423
- }
424
- });
425
- }
558
+ if (/\.(?:md|markdown)$/iu.test(path)) return /* @__PURE__ */ jsx(MarkdownPreview, {
559
+ ...props,
560
+ path,
561
+ working
562
+ }, working.hash);
426
563
  return /* @__PURE__ */ jsx(CodeBlock, {
427
564
  text: working.content,
428
565
  language: path.split(".").at(-1)
@@ -446,35 +583,6 @@ const styles$2 = StyleSheet.create((theme) => ({
446
583
  }));
447
584
  //#endregion
448
585
  //#region src/extensions/workspaces/sessions/(frontend)/_components/FilePanel.native.tsx
449
- function Diff({ hunks, comment }) {
450
- return /* @__PURE__ */ jsx(View, {
451
- style: styles$1.diff,
452
- children: hunks.map((hunk, index) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
453
- style: styles$1.meta,
454
- children: ["line ", hunk.start]
455
- }), hunk.rows.map((row, position) => /* @__PURE__ */ jsx(Button, {
456
- variant: "ghost",
457
- accessibilityLabel: `comment on ${row.marker === "-" ? "removed" : "new"} line ${row.line}`,
458
- onPress: () => comment({
459
- snippet: row.content,
460
- side: row.marker === "-" ? "old" : "new",
461
- startLine: row.line,
462
- endLine: row.line
463
- }),
464
- children: /* @__PURE__ */ jsxs(Text, {
465
- selectable: true,
466
- style: row.marker === "+" ? styles$1.added : row.marker === "-" ? styles$1.removed : styles$1.code,
467
- children: [
468
- row.marker,
469
- " ",
470
- row.line,
471
- " ",
472
- row.content
473
- ]
474
- })
475
- }, `${row.line}-${position}`))] }, `${hunk.start}-${index}`))
476
- });
477
- }
478
586
  function ReviewSheet({ selection, close, submit }) {
479
587
  const [body, setBody] = useState("");
480
588
  const [snippet, setSnippet] = useState(selection.snippet);
@@ -514,6 +622,23 @@ function ReviewSheet({ selection, close, submit }) {
514
622
  ] })] })
515
623
  });
516
624
  }
625
+ /** Cache the selected file snapshot, not the session record or a freshly filtered comments array. */
626
+ function fileSnapshot(sessionId, filePath) {
627
+ let previous;
628
+ return () => {
629
+ const session = files.select(files.store.state, sessionId);
630
+ const next = {
631
+ detail: session.detail[filePath],
632
+ loading: session.loading.includes(filePath),
633
+ error: session.errors[filePath],
634
+ revision: session.items.find((item) => item.path === filePath)?.count ?? 0,
635
+ comments: session.comments.filter((comment) => comment.path === filePath)
636
+ };
637
+ if (previous && previous.detail === next.detail && previous.loading === next.loading && previous.error === next.error && previous.revision === next.revision && previous.comments.length === next.comments.length && previous.comments.every((comment, index) => comment === next.comments[index])) return previous;
638
+ previous = next;
639
+ return next;
640
+ };
641
+ }
517
642
  /** File writes retain the existing containment and stale-hash guards; deletion always asks first. */
518
643
  function FilePanel({ filePath, relPath, ...props }) {
519
644
  const { sessionId, sendSessionFrame, closeTransientTab } = props;
@@ -521,10 +646,9 @@ function FilePanel({ filePath, relPath, ...props }) {
521
646
  const subscription = files.store.subscribe(listener);
522
647
  return () => subscription.unsubscribe();
523
648
  }, []);
524
- const session = useSyncExternalStore(subscribe, () => files.select(files.store.state, sessionId));
525
- const detail = session.detail[filePath];
649
+ const snapshot = useMemo(() => fileSnapshot(sessionId, filePath), [sessionId, filePath]);
650
+ const { detail, loading, error, revision, comments } = useSyncExternalStore(subscribe, snapshot);
526
651
  const working = detail?.working;
527
- const comments = session.comments.filter((comment) => comment.path === filePath);
528
652
  const [view, setView] = useState("preview");
529
653
  const [source, setSource] = useState();
530
654
  const [expanded, setExpanded] = useState();
@@ -536,7 +660,6 @@ function FilePanel({ filePath, relPath, ...props }) {
536
660
  const [reviewError, setReviewError] = useState();
537
661
  const outside = relPath === ".." || relPath.startsWith("../") || relPath.startsWith("/") || relPath.startsWith("..\\") || /^[a-z]:[\\/]/iu.test(relPath);
538
662
  const writable = sessionId !== null && !outside && working !== void 0 && !working.unavailable;
539
- const revision = session.items.find((item) => item.path === filePath)?.count ?? 0;
540
663
  useEffect(() => {
541
664
  if (sessionId === null) return;
542
665
  let cancelled = false;
@@ -693,14 +816,14 @@ function FilePanel({ filePath, relPath, ...props }) {
693
816
  style: styles$1.meta,
694
817
  children: "editing"
695
818
  }) : null,
696
- session.loading.includes(filePath) && !detail ? /* @__PURE__ */ jsx(Text, {
819
+ loading && !detail ? /* @__PURE__ */ jsx(Text, {
697
820
  style: styles$1.meta,
698
821
  children: "reading this file's history…"
699
822
  }) : null,
700
- session.errors[filePath] ? /* @__PURE__ */ jsx(Text, {
823
+ error ? /* @__PURE__ */ jsx(Text, {
701
824
  accessibilityRole: "alert",
702
825
  style: styles$1.error,
703
- children: session.errors[filePath]
826
+ children: error
704
827
  }) : null,
705
828
  detail ? view === "diff" ? /* @__PURE__ */ jsxs(Fragment, { children: [
706
829
  /* @__PURE__ */ jsxs(Text, {
@@ -716,10 +839,10 @@ function FilePanel({ filePath, relPath, ...props }) {
716
839
  style: styles$1.meta,
717
840
  children: detail.cumulative.note
718
841
  }) : null,
719
- detail.cumulative.hunks ? /* @__PURE__ */ jsx(Diff, {
842
+ detail.cumulative.hunks ? /* @__PURE__ */ jsx(FileDiff, {
720
843
  hunks: detail.cumulative.hunks,
721
844
  comment: setDraft
722
- }) : null,
845
+ }, `${sessionId}:${filePath}`) : null,
723
846
  /* @__PURE__ */ jsxs(Text, {
724
847
  style: styles$1.title,
725
848
  children: [detail.versions.length, " changes this session"]
@@ -739,10 +862,10 @@ function FilePanel({ filePath, relPath, ...props }) {
739
862
  version.removals,
740
863
  version.origin === "scan" ? " · no baseline" : ""
741
864
  ]
742
- }), expanded === version.index ? version.hunks ? /* @__PURE__ */ jsx(Diff, {
865
+ }), expanded === version.index ? version.hunks ? /* @__PURE__ */ jsx(FileDiff, {
743
866
  hunks: version.hunks,
744
867
  comment: setDraft
745
- }) : /* @__PURE__ */ jsx(Text, {
868
+ }, `${sessionId}:${filePath}`) : /* @__PURE__ */ jsx(Text, {
746
869
  style: styles$1.meta,
747
870
  children: version.note ?? "nothing to show"
748
871
  }) : null] }, version.index))
@@ -926,24 +1049,6 @@ const styles$1 = StyleSheet.create((theme) => ({
926
1049
  color: theme.colors.hi,
927
1050
  uni__dependencies: [0]
928
1051
  },
929
- code: {
930
- fontFamily: fontOf(theme.fontFamily.mono),
931
- fontSize: theme.fontSize.sm,
932
- color: theme.colors.text,
933
- uni__dependencies: [0]
934
- },
935
- added: {
936
- fontFamily: fontOf(theme.fontFamily.mono),
937
- fontSize: theme.fontSize.sm,
938
- color: theme.colors.green,
939
- uni__dependencies: [0]
940
- },
941
- removed: {
942
- fontFamily: fontOf(theme.fontFamily.mono),
943
- fontSize: theme.fontSize.sm,
944
- color: theme.colors.red,
945
- uni__dependencies: [0]
946
- },
947
1052
  meta: {
948
1053
  fontSize: theme.fontSize.sm,
949
1054
  color: theme.colors.dim,