@uniqbit/mate-core 0.18.1-canary.4 → 0.18.1

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.
@@ -44,6 +44,8 @@ export interface VaultWatchEvent {
44
44
 
45
45
  export interface VaultTreeResult {
46
46
  tree: VaultTreeNode[];
47
+ /** Counts watch invalidations seen when this listing was taken; a larger one makes it stale. */
48
+ generation: number;
47
49
  watching: boolean;
48
50
  warning: string | null;
49
51
  }
@@ -379,6 +381,11 @@ interface SaveRecord {
379
381
  expires: ReturnType<typeof setTimeout>;
380
382
  }
381
383
 
384
+ interface ListedTree {
385
+ tree: VaultTreeNode[];
386
+ generation: number;
387
+ }
388
+
382
389
  interface Subscriber {
383
390
  path: string;
384
391
  notify: (event: VaultWatchEvent) => void;
@@ -400,6 +407,8 @@ export interface VaultManager {
400
407
  requestedPath: string,
401
408
  notify: (event: VaultWatchEvent) => void,
402
409
  ): () => void;
410
+ /** Reports the listing generation now and on every later invalidation; carries no paths or content. */
411
+ subscribeTree(companionRoot: string, notify: (generation: number) => void): () => void;
403
412
  refresh(companionRoot: string): Promise<VaultTreeResult>;
404
413
  deactivate(): void;
405
414
  stop(): void;
@@ -412,7 +421,8 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
412
421
  const git = deps.git ?? runGit;
413
422
  const trees = new Map<string, VaultTreeNode[]>();
414
423
  const generations = new Map<string, number>();
415
- const listings = new Map<string, Promise<VaultTreeNode[]>>();
424
+ const listings = new Map<string, Promise<ListedTree>>();
425
+ const treeSubscribers = new Map<string, Set<(generation: number) => void>>();
416
426
  const listed = new Map<string, string[]>();
417
427
  const warnings = new Map<string, string | null>();
418
428
  const watchers = new Map<string, Map<string, VaultWatcher>>();
@@ -430,7 +440,9 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
430
440
 
431
441
  const invalidate = (root: string) => {
432
442
  trees.delete(root);
433
- generations.set(root, (generations.get(root) ?? 0) + 1);
443
+ const generation = (generations.get(root) ?? 0) + 1;
444
+ generations.set(root, generation);
445
+ for (const notifyTree of treeSubscribers.get(root) ?? []) notifyTree(generation);
434
446
  };
435
447
 
436
448
  const notify = async (
@@ -624,7 +636,7 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
624
636
  };
625
637
 
626
638
  /** A change seen while listing leaves the cache empty, so the next request lists again. */
627
- const listTree = (root: string): Promise<VaultTreeNode[]> => {
639
+ const listTree = (root: string): Promise<ListedTree> => {
628
640
  const inFlight = listings.get(root);
629
641
  if (inFlight) return inFlight;
630
642
  const generation = generations.get(root) ?? 0;
@@ -636,7 +648,7 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
636
648
  scheduleReconcile(root, files);
637
649
  const tree = asTree(files);
638
650
  if ((generations.get(root) ?? 0) === generation) trees.set(root, tree);
639
- return tree;
651
+ return { tree, generation };
640
652
  } finally {
641
653
  listings.delete(root);
642
654
  }
@@ -761,11 +773,13 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
761
773
  return {
762
774
  async tree(companionRoot, refresh = false) {
763
775
  const root = await rootKey(companionRoot);
764
- const tree = !refresh && trees.get(root);
765
- const result = tree || (await listTree(root));
776
+ const cached = !refresh && trees.get(root);
777
+ const result: ListedTree = cached
778
+ ? { tree: cached, generation: generations.get(root) ?? 0 }
779
+ : await listTree(root);
766
780
  startWatching(root);
767
781
  return {
768
- tree: result,
782
+ ...result,
769
783
  watching: watchers.has(root),
770
784
  warning: warnings.get(root) ?? null,
771
785
  };
@@ -775,7 +789,14 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
775
789
  if (refresh) invalidate(root);
776
790
  startWatching(root);
777
791
  const tree = trees.get(root);
778
- if (tree) return { tree, watching: watchers.has(root), warning: warnings.get(root) ?? null };
792
+ if (tree) {
793
+ return {
794
+ tree,
795
+ generation: generations.get(root) ?? 0,
796
+ watching: watchers.has(root),
797
+ warning: warnings.get(root) ?? null,
798
+ };
799
+ }
779
800
  void listTree(root).catch(() => {});
780
801
  return null;
781
802
  },
@@ -818,6 +839,30 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
818
839
  if (currentSubscribers.size === 0) subscribers.delete(key);
819
840
  };
820
841
  },
842
+ subscribeTree(companionRoot, notifyTree) {
843
+ let active = true;
844
+ let subscribedRoot: string | null = null;
845
+ void rootKey(companionRoot).then(
846
+ (root) => {
847
+ if (!active) return;
848
+ subscribedRoot = root;
849
+ const set = treeSubscribers.get(root) ?? new Set();
850
+ set.add(notifyTree);
851
+ treeSubscribers.set(root, set);
852
+ startWatching(root);
853
+ if (!trees.has(root)) void listTree(root).catch(() => {});
854
+ notifyTree(generations.get(root) ?? 0);
855
+ },
856
+ () => {},
857
+ );
858
+ return () => {
859
+ active = false;
860
+ if (!subscribedRoot) return;
861
+ const set = treeSubscribers.get(subscribedRoot);
862
+ set?.delete(notifyTree);
863
+ if (set?.size === 0) treeSubscribers.delete(subscribedRoot);
864
+ };
865
+ },
821
866
  async refresh(companionRoot) {
822
867
  return this.tree(companionRoot, true);
823
868
  },
@@ -828,6 +873,7 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
828
873
  }
829
874
  activeRoot = null;
830
875
  subscribers.clear();
876
+ treeSubscribers.clear();
831
877
  },
832
878
  stop() {
833
879
  for (const root of watchers.keys()) closeWatchers(root);
@@ -838,6 +884,7 @@ export function createVaultManager(deps: VaultDeps = {}): VaultManager {
838
884
  pendingEvents.clear();
839
885
  recentSaves.clear();
840
886
  subscribers.clear();
887
+ treeSubscribers.clear();
841
888
  },
842
889
  getRecovery(companionRoot, requestedPath) {
843
890
  const root = path.resolve(companionRoot);
@@ -1,3 +1,4 @@
1
+ import { REPORT_VIEW_PREFIX } from "../routes";
1
2
  import { COMPANION_DIGEST_PATTERN, COMPANION_PARAM } from "../selection";
2
3
 
3
4
  export const THEME_STORAGE_KEY = "mate-studio-theme";
@@ -203,6 +204,35 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
203
204
  });
204
205
  });
205
206
 
207
+ /* Delegated: a spec row's controls need no per-node wiring. */
208
+ var AGENT_ORDER = ["claude", "opencode"];
209
+ function copyPromptFor(node) {
210
+ var viewed = document.documentElement.getAttribute("data-agent-viewed");
211
+ var order = viewed ? [viewed].concat(AGENT_ORDER) : AGENT_ORDER;
212
+ for (var index = 0; index < order.length; index += 1) {
213
+ var text = node.getAttribute("data-prompt-" + order[index]);
214
+ if (text) return text;
215
+ }
216
+ return null;
217
+ }
218
+ if (typeof document.addEventListener === "function") document.addEventListener("click", function (event) {
219
+ var target = event.target;
220
+ if (!target || typeof target.closest !== "function") return;
221
+ var run = target.closest("[data-studio-action]");
222
+ if (run) {
223
+ var agents = (run.getAttribute("data-studio-agents") || "").split(" ").filter(Boolean);
224
+ document.dispatchEvent(new CustomEvent("studio:terminal-action", {
225
+ detail: { action: run.getAttribute("data-studio-action"), subject: run.getAttribute("data-studio-subject"), agents: agents },
226
+ }));
227
+ return;
228
+ }
229
+ var prompt = target.closest("[data-studio-copy-prompt]");
230
+ if (prompt) {
231
+ var text = copyPromptFor(prompt);
232
+ if (text) copy(text, "prompt");
233
+ }
234
+ });
235
+
206
236
  var savePromise = null;
207
237
  function editor() { return document.getElementById("vault-editor"); }
208
238
  function status(message) {
@@ -253,7 +283,7 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
253
283
  return target.toString();
254
284
  }
255
285
  function wireNavigation(scope) {
256
- (scope || document).querySelectorAll('form[method="get"]').forEach(function (form) {
286
+ (scope || document).querySelectorAll('form[method="get"]:not([data-vault-refresh])').forEach(function (form) {
257
287
  form.addEventListener("submit", function (event) {
258
288
  event.preventDefault();
259
289
  navigate(navigationUrl(form, event.submitter));
@@ -335,22 +365,6 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
335
365
  event.preventDefault();
336
366
  event.returnValue = "";
337
367
  });
338
- function filterTree(query) {
339
- var needle = query.trim().toLowerCase();
340
- document.querySelectorAll("[data-vault-entry]").forEach(function (entry) {
341
- entry.hidden = !!needle && (entry.getAttribute("data-vault-entry") || "").toLowerCase().indexOf(needle) === -1;
342
- });
343
- document.querySelectorAll("[data-vault-dir]").forEach(function (folder) {
344
- if (!needle) {
345
- folder.hidden = false;
346
- folder.open = folder.hasAttribute("data-vault-expanded");
347
- return;
348
- }
349
- var match = folder.querySelector("[data-vault-entry]:not([hidden])");
350
- folder.hidden = !match;
351
- if (match) folder.open = true;
352
- });
353
- }
354
368
  function showTree(shown) {
355
369
  var root = document.documentElement;
356
370
  if (shown) root.removeAttribute("data-vault-tree");
@@ -366,18 +380,67 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
366
380
  /* a blocked web store only costs the preference, never the page */
367
381
  }
368
382
  }
369
- /** Moves server-rendered parts into place; builds no markup of its own. */
383
+ var displayedGeneration = null;
384
+ var latestGeneration = null;
385
+ var refreshSequence = 0;
386
+ var filterSequence = 0;
387
+ var filterTimer = null;
388
+ var filterRequest = null;
389
+ var FILTER_DELAY = 150;
390
+
391
+ function failure(error) {
392
+ return error && error.message ? error.message : String(error);
393
+ }
394
+ function fetchMarkup(url, signal) {
395
+ return fetch(url, { headers: { accept: "text/html" }, signal: signal }).then(function (response) {
396
+ if (!response.ok) throw new Error("status " + response.status);
397
+ return response.text();
398
+ });
399
+ }
400
+ /** Moves server-rendered nodes into a container; builds no markup of its own. */
401
+ function moveInto(container, markup) {
402
+ var parsed = new DOMParser().parseFromString(markup, "text/html");
403
+ container.replaceChildren.apply(
404
+ container,
405
+ Array.prototype.map.call(parsed.body.childNodes, function (child) { return document.importNode(child, true); }),
406
+ );
407
+ }
408
+ function readGeneration(slot) {
409
+ var value = slot ? slot.getAttribute("data-vault-generation") : null;
410
+ return value === null || value === "" ? null : Number(value);
411
+ }
412
+ /** The indicator clears only once the displayed listing is as new as the newest change seen. */
413
+ function showStale() {
414
+ var note = document.getElementById("vault-stale");
415
+ if (!note) return;
416
+ note.hidden = !(latestGeneration !== null && displayedGeneration !== null && latestGeneration > displayedGeneration);
417
+ }
418
+ function openedByViewer() {
419
+ var keys = [];
420
+ document.querySelectorAll("details[data-vault-children-url][open]").forEach(function (folder) {
421
+ keys.push(folder.getAttribute("data-vault-dir"));
422
+ });
423
+ return keys;
424
+ }
425
+ /** Swaps server-rendered parts into place; a refreshed tree drops loaded folders and reopens the viewer's. */
370
426
  function swapVaultSlots(markup) {
427
+ var reopen = openedByViewer();
371
428
  var parsed = new DOMParser().parseFromString(markup, "text/html");
372
429
  parsed.querySelectorAll("[data-vault-slot]").forEach(function (slot) {
373
- var target = document.querySelector('[data-vault-slot="' + slot.getAttribute("data-vault-slot") + '"]');
430
+ var name = slot.getAttribute("data-vault-slot");
431
+ var target = document.querySelector('[data-vault-slot="' + name + '"]');
374
432
  if (!target) return;
375
433
  var node = document.importNode(slot, true);
376
434
  target.replaceWith(node);
377
435
  wireNavigation(node);
436
+ if (name !== "tree") return;
437
+ displayedGeneration = readGeneration(node);
438
+ node.querySelectorAll("details[data-vault-children-url]").forEach(function (folder) {
439
+ if (reopen.indexOf(folder.getAttribute("data-vault-dir")) !== -1) folder.open = true;
440
+ });
378
441
  });
379
- var filter = document.getElementById("vault-filter");
380
- if (filter && filter.value) filterTree(filter.value);
442
+ showStale();
443
+ runFilter();
381
444
  }
382
445
  function failVaultSlots(message) {
383
446
  document.querySelectorAll("[data-vault-slot][aria-busy]").forEach(function (slot) {
@@ -385,13 +448,109 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
385
448
  slot.textContent = message;
386
449
  });
387
450
  }
451
+ function loadChildren(folder) {
452
+ var url = folder.getAttribute("data-vault-children-url");
453
+ var container = folder.querySelector(".vault-children");
454
+ if (!url || !container || folder.hasAttribute("data-vault-loaded") || folder.hasAttribute("data-vault-loading")) return;
455
+ folder.setAttribute("data-vault-loading", "");
456
+ fetchMarkup(url).then(function (markup) {
457
+ moveInto(container, markup);
458
+ folder.setAttribute("data-vault-loaded", "");
459
+ }).catch(function (error) {
460
+ container.textContent = "This folder could not be loaded: " + failure(error);
461
+ }).finally(function () { folder.removeAttribute("data-vault-loading"); });
462
+ }
463
+ function runFilter() {
464
+ var filter = document.getElementById("vault-filter");
465
+ var results = document.getElementById("vault-filter-results");
466
+ var panel = document.getElementById("vault-tree-panel");
467
+ var layout = document.querySelector("[data-vault-layout]");
468
+ if (!filter || !results || !panel || !layout) return;
469
+ clearTimeout(filterTimer);
470
+ if (filterRequest) filterRequest.abort();
471
+ var needle = filter.value.trim();
472
+ filterSequence += 1;
473
+ if (!needle) {
474
+ results.hidden = true;
475
+ results.replaceChildren();
476
+ panel.removeAttribute("data-vault-filtering");
477
+ return;
478
+ }
479
+ var url = new URL(layout.getAttribute("data-vault-filter-url") || "", location.href);
480
+ url.searchParams.set("q", needle);
481
+ var sequence = filterSequence;
482
+ filterRequest = typeof AbortController !== "undefined" ? new AbortController() : null;
483
+ fetchMarkup(url.toString(), filterRequest ? filterRequest.signal : undefined).then(function (markup) {
484
+ if (sequence !== filterSequence) return;
485
+ moveInto(results, markup);
486
+ results.hidden = false;
487
+ panel.setAttribute("data-vault-filtering", "");
488
+ }).catch(function (error) {
489
+ if (sequence !== filterSequence || (error && error.name === "AbortError")) return;
490
+ results.textContent = "The filter could not run: " + failure(error);
491
+ results.hidden = false;
492
+ panel.setAttribute("data-vault-filtering", "");
493
+ });
494
+ }
495
+ /** Replaces the tree, listing and filter results in place; the editor and terminal are left alone. */
496
+ function refreshTree() {
497
+ var layout = document.querySelector("[data-vault-layout]");
498
+ var status = document.getElementById("vault-refresh-status");
499
+ var url = layout ? layout.getAttribute("data-vault-refresh-url") : null;
500
+ if (!url) return;
501
+ refreshSequence += 1;
502
+ var sequence = refreshSequence;
503
+ if (status) status.hidden = true;
504
+ fetchMarkup(url).then(function (markup) {
505
+ if (sequence === refreshSequence) swapVaultSlots(markup);
506
+ }).catch(function (error) {
507
+ if (sequence !== refreshSequence || !status) return;
508
+ status.textContent = "The tree could not be refreshed: " + failure(error) + ". Select Refresh tree to retry.";
509
+ status.hidden = false;
510
+ });
511
+ }
512
+ function watchTreeChanges(layout) {
513
+ var url = layout.getAttribute("data-vault-changes-url");
514
+ if (!url || typeof EventSource === "undefined") return;
515
+ var changes = new EventSource(url);
516
+ changes.onmessage = function (message) {
517
+ var event;
518
+ try { event = JSON.parse(message.data); } catch (error) { return; }
519
+ if (!event || event.type !== "vault-tree-changed" || typeof event.generation !== "number") return;
520
+ if (latestGeneration === null || event.generation > latestGeneration) latestGeneration = event.generation;
521
+ showStale();
522
+ };
523
+ }
388
524
  function wireVaultBrowser() {
389
525
  var layout = document.querySelector("[data-vault-layout]");
390
526
  if (!layout) return;
527
+ var panel = document.getElementById("vault-tree-panel");
528
+ displayedGeneration = readGeneration(document.querySelector('[data-vault-slot="tree"]'));
529
+ if (panel) {
530
+ panel.addEventListener("click", function (event) {
531
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
532
+ var link = event.target && event.target.closest ? event.target.closest("a[data-vault-entry]") : null;
533
+ if (!link) return;
534
+ event.preventDefault();
535
+ navigate(link.href);
536
+ });
537
+ panel.addEventListener("toggle", function (event) {
538
+ var folder = event.target;
539
+ if (folder && folder.open && folder.matches && folder.matches("details[data-vault-children-url]")) loadChildren(folder);
540
+ }, true);
541
+ }
542
+ var refresh = document.querySelector("form[data-vault-refresh]");
543
+ if (refresh) refresh.addEventListener("submit", function (event) {
544
+ event.preventDefault();
545
+ refreshTree();
546
+ });
391
547
  var filter = document.getElementById("vault-filter");
392
548
  if (filter) {
393
549
  filter.hidden = false;
394
- filter.addEventListener("input", function () { filterTree(filter.value); });
550
+ filter.addEventListener("input", function () {
551
+ clearTimeout(filterTimer);
552
+ filterTimer = setTimeout(runFilter, FILTER_DELAY);
553
+ });
395
554
  }
396
555
  var toggle = document.getElementById("vault-tree-toggle");
397
556
  if (toggle) {
@@ -403,16 +562,93 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
403
562
  showTree(document.documentElement.getAttribute("data-vault-tree") === "hidden");
404
563
  });
405
564
  }
565
+ watchTreeChanges(layout);
406
566
  var deferred = layout.getAttribute("data-vault-view");
407
567
  if (!deferred) return;
408
- fetch(deferred, { headers: { accept: "text/html" } }).then(function (response) {
409
- if (!response.ok) throw new Error("status " + response.status);
410
- return response.text();
411
- }).then(swapVaultSlots).catch(function (error) {
412
- failVaultSlots("The files could not be listed: " + (error && error.message ? error.message : String(error)));
568
+ fetchMarkup(deferred).then(swapVaultSlots).catch(function (error) {
569
+ failVaultSlots("The files could not be listed: " + failure(error));
413
570
  });
414
571
  }
572
+ /** Hosted reports: the list, the sandboxed frame, and the toast and badge for a new arrival. */
573
+ function wireReports() {
574
+ var shell = document.querySelector("[data-reports-events-url]");
575
+ if (!shell || typeof EventSource === "undefined") return;
576
+ var current = shell.getAttribute("data-companion");
577
+ var listUrl = shell.getAttribute("data-reports-url");
578
+ var viewUrl = shell.getAttribute("data-reports-view-url");
579
+ var list = document.getElementById("reports-list");
580
+ var frame = document.getElementById("reports-frame");
581
+ var open = document.getElementById("reports-open");
582
+ var empty = document.getElementById("reports-empty");
583
+ var template = document.getElementById("reports-item-template");
584
+ var badge = document.getElementById("reports-badge");
585
+ var toast = document.getElementById("reports-toast");
586
+ var toastTitle = document.getElementById("reports-toast-title");
587
+ var toastOpen = document.getElementById("reports-toast-open");
588
+ var shown = null;
589
+ var toastTimeout;
590
+ function reportHash() {
591
+ var match = /(?:^|[#&])report=([0-9a-f]{32})/.exec(location.hash || "");
592
+ return match ? match[1] : null;
593
+ }
594
+ function show(id) {
595
+ if (!frame) return;
596
+ shown = id;
597
+ var src = ${JSON.stringify(REPORT_VIEW_PREFIX)} + id;
598
+ frame.src = src;
599
+ frame.hidden = false;
600
+ if (open) { open.href = src; open.hidden = false; }
601
+ if (list) list.querySelectorAll("button[data-report-id]").forEach(function (button) {
602
+ button.setAttribute("aria-pressed", button.getAttribute("data-report-id") === id ? "true" : "false");
603
+ });
604
+ try { history.replaceState(null, "", "#report=" + id); } catch (error) { /* the address is a convenience */ }
605
+ }
606
+ function render(reports) {
607
+ if (!list || !template || !template.content.firstElementChild) return;
608
+ list.replaceChildren();
609
+ if (empty) empty.hidden = reports.length > 0;
610
+ reports.forEach(function (report) {
611
+ var item = template.content.firstElementChild.cloneNode(true);
612
+ var button = item.querySelector("button");
613
+ button.setAttribute("data-report-id", report.id);
614
+ button.setAttribute("aria-pressed", report.id === shown ? "true" : "false");
615
+ item.querySelector("[data-report-title]").textContent = report.title;
616
+ item.querySelector("[data-report-time]").textContent = new Date(report.createdAt).toLocaleString();
617
+ button.addEventListener("click", function () { show(report.id); });
618
+ list.append(item);
619
+ });
620
+ if (reports.length === 0 || reports.some(function (report) { return report.id === shown; })) return;
621
+ var named = reportHash();
622
+ show(reports.some(function (report) { return report.id === named; }) ? named : reports[0].id);
623
+ }
624
+ function refreshList() {
625
+ if (!list || !listUrl) return;
626
+ fetch(listUrl, { headers: { accept: "application/json" } }).then(function (response) {
627
+ if (!response.ok) throw new Error("status " + response.status);
628
+ return response.json();
629
+ }).then(function (body) { render(body.reports || []); }).catch(function () { /* the next event or reconnect retries */ });
630
+ }
631
+ function announceReport(event) {
632
+ if (badge) badge.hidden = false;
633
+ if (!toast || !toastTitle) return;
634
+ toastTitle.textContent = "New report: " + event.title;
635
+ if (toastOpen && viewUrl) toastOpen.href = viewUrl + "#report=" + event.id;
636
+ toast.setAttribute("data-shown", "true");
637
+ clearTimeout(toastTimeout);
638
+ toastTimeout = setTimeout(function () { toast.setAttribute("data-shown", "false"); }, 10000);
639
+ }
640
+ var events = new EventSource(shell.getAttribute("data-reports-events-url"));
641
+ events.onopen = refreshList;
642
+ events.onmessage = function (message) {
643
+ var event;
644
+ try { event = JSON.parse(message.data); } catch (error) { return; }
645
+ if (!event || event.type !== "report-published" || event.companion !== current) return;
646
+ if (list) refreshList();
647
+ else announceReport(event);
648
+ };
649
+ }
415
650
  wireNavigation();
416
651
  wireVault();
417
652
  wireVaultBrowser();
653
+ wireReports();
418
654
  })();`;