@oharness/server 0.12.0 → 0.14.0

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.
@@ -441,6 +441,10 @@ async function showMemoryPage(id) {
441
441
  }
442
442
  // Nothing to tidy is not worth a row.
443
443
  if (findings.length || consolidate.lastAuto) panel.appendChild(tidy);
444
+ // Supplied by the dreaming plugin; absent when it is switched off. Its own
445
+ // page holds the detail; here, one line and the way there.
446
+ const dream = result.sections && result.sections.dreaming;
447
+ if (dream) panel.appendChild(dreamCard(dream));
444
448
  const editor = (note, extra) => {
445
449
  const form = el("form", "memory-editor"), summary = el("input", ""), body = el("textarea", ""), tags = el("input", "");
446
450
  summary.placeholder = t("memory.summaryPlaceholder"); summary.value = note?.summary || ""; summary.required = true;
@@ -604,7 +608,7 @@ async function showMemoryPage(id) {
604
608
  * has a row of its own on the rail as well as Settings', and lights that one
605
609
  * whichever way it was reached.
606
610
  */
607
- const SUB_PANELS = { usage: "nav-usage", audit: "nav-settings", permissions: "nav-settings" };
611
+ const SUB_PANELS = { usage: "nav-usage", audit: "nav-settings", permissions: "nav-settings", dreaming: "nav-settings" };
608
612
 
609
613
  /** Which panel the panel view is currently showing, sub-panels included. */
610
614
  let shownPanel = null;
@@ -617,6 +621,226 @@ let shownPanel = null;
617
621
  * left intact underneath it, and the composer is hidden — there is nothing to
618
622
  * send a message to while reading a list of providers.
619
623
  */
624
+ /** Spent money, at the precision a small amount needs. */
625
+ function dreamCost(usd) {
626
+ return "$" + (usd || 0).toFixed(usd > 0 && usd < 0.01 ? 4 : 2);
627
+ }
628
+
629
+ /** Whether a timestamp falls in this local calendar month. */
630
+ function inThisMonth(at) {
631
+ const d = new Date(at), now = new Date();
632
+ return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
633
+ }
634
+
635
+ /** The Memory page's line about dreaming, and the way to its page. */
636
+ function dreamCard(dream) {
637
+ const card = el("div", "row-card dream-card");
638
+ const head = el("div", "row-head");
639
+ head.appendChild(el("span", "row-title", t("dream.title")));
640
+ head.appendChild(el("span", "memory-dream-tag " + (dream.auto ? "apply" : ""), t(dream.auto ? "dream.auto" : "dream.manual")));
641
+ const open = el("button", "ghost", t("dream.open")); open.type = "button";
642
+ open.onclick = () => { setNav("dreaming"); void showDreaming(); };
643
+ head.appendChild(open);
644
+ card.appendChild(head);
645
+ const facts = [];
646
+ if ((dream.proposals || []).length) facts.push(t("dream.waitingCount", { n: dream.proposals.length }));
647
+ facts.push(t("dream.monthSpend", { tokens: formatTokens((dream.spend && dream.spend.tokens) || 0), cost: dreamCost(dream.spend && dream.spend.costUsd) }));
648
+ card.appendChild(el("div", "muted small", facts.join(" · ")));
649
+ return card;
650
+ }
651
+
652
+ /** The switch for whether dreaming runs on its own. */
653
+ function dreamAutoSwitch(on, onChange) {
654
+ const button = el("button", "switch");
655
+ button.type = "button";
656
+ button.setAttribute("role", "switch");
657
+ button.setAttribute("aria-checked", on ? "true" : "false");
658
+ button.setAttribute("aria-label", t("dream.autoSwitch"));
659
+ button.title = t(on ? "dream.autoOnHint" : "dream.autoOffHint");
660
+ button.addEventListener("click", (event) => {
661
+ // Inside a row's summary, a click would also open or close the row.
662
+ event.preventDefault(); event.stopPropagation();
663
+ button.disabled = true;
664
+ void Promise.resolve(onChange(!on));
665
+ });
666
+ return button;
667
+ }
668
+
669
+ /** When the next automatic dream happens, scope by scope, in words. */
670
+ function dreamNext(dream) {
671
+ if (!dream.auto) return t("dream.next.manual");
672
+ const settings = dream.settings || {};
673
+ const parts = [];
674
+ for (const scope of (dream.schedule && dream.schedule.scopes) || []) {
675
+ let state;
676
+ if (scope.dreamedToday) state = t("dream.next.doneToday");
677
+ else if (!scope.waiting) state = t("dream.next.nothing");
678
+ else if (scope.waiting >= settings.minClaims || (scope.oldestAt && Date.now() - scope.oldestAt >= settings.maxWaitHours * 3600000)) state = t("dream.next.due");
679
+ else {
680
+ const hours = Math.max(1, Math.ceil((scope.oldestAt + settings.maxWaitHours * 3600000 - Date.now()) / 3600000));
681
+ state = t("dream.next.gathering", { n: scope.waiting, min: settings.minClaims, hours });
682
+ }
683
+ parts.push(t("memory.dream.scope." + scope.scope) + ": " + state);
684
+ }
685
+ return parts.join(" · ");
686
+ }
687
+
688
+ /** Before and after, side by side, for one change. */
689
+ function dreamDiff(change) {
690
+ const box = el("div", "dream-diff");
691
+ const side = (label, notes, empty) => {
692
+ const col = el("div", "dream-diff-side");
693
+ col.appendChild(el("div", "muted small", label));
694
+ if (!notes.length) col.appendChild(el("div", "muted small", empty));
695
+ for (const note of notes) {
696
+ if (note.removed) col.appendChild(el("div", "muted small", t("dream.diff.removed")));
697
+ else if (note.editedSince) col.appendChild(el("div", "muted small", t("dream.diff.editedSince")));
698
+ else col.appendChild(el("pre", "memory-dream-body", note.summary + "\n\n" + note.body));
699
+ }
700
+ return col;
701
+ };
702
+ box.appendChild(side(t("memory.dream.before"), (change.before || []).filter((note) => note.body), t("dream.diff.none")));
703
+ box.appendChild(side(t("memory.dream.after"), change.after || [], t("dream.diff.none")));
704
+ return box;
705
+ }
706
+
707
+ /**
708
+ * Dreaming's own page: whether it runs and when next, what this month cost,
709
+ * the suggestions waiting, and every run, each with what it changed.
710
+ */
711
+ async function showDreaming() {
712
+ const session = activeId;
713
+ const page = [...pluginPages.values()].find((one) => one.renderer === "memory");
714
+ const result = page ? await getJson("/plugin-page?id=" + encodeURIComponent(page.id) + "&session=" + encodeURIComponent(session || "")) : {};
715
+ if (shownPanel !== "dreaming" || activeId !== session) return;
716
+ panelBody.replaceChildren(); panelNotes.replaceChildren();
717
+ const panel = el("div", "panel memory-panel dream-panel"); panelBody.appendChild(panel);
718
+ const dream = result.sections && result.sections.dreaming;
719
+ const head = el("div", "dream-head");
720
+ head.appendChild(el("h2", "", t("dream.title")));
721
+ panel.appendChild(head);
722
+ if (!dream) { panel.appendChild(el("p", "muted", t("dream.off"))); return; }
723
+ head.appendChild(el("span", "dream-spacer"));
724
+ // Whether it runs on its own: the user's choice, kept in their plugin config.
725
+ const autoLabel = el("label", "dream-auto");
726
+ autoLabel.appendChild(el("span", "small", t("dream.autoSwitch")));
727
+ const autoSwitch = dreamAutoSwitch(dream.auto, (next) => act({ action: "setAuto", auto: next }));
728
+ autoLabel.appendChild(autoSwitch);
729
+ head.appendChild(autoLabel);
730
+ const act = async (input) => {
731
+ try {
732
+ const reply = await api("/plugin-page", { page: page.id, session, input: { action: "section", section: "dreaming", input } }, 120000);
733
+ if (reply.error) throw Object.assign(new Error(reply.error), { code: reply.code, params: reply.params });
734
+ } catch (error) {
735
+ if (shownPanel === "dreaming") panelNotes.appendChild(el("p", "small memory-error", memoryText(error)));
736
+ return;
737
+ }
738
+ if (shownPanel === "dreaming" && activeId === session) await showDreaming();
739
+ };
740
+ const now = el("button", "ghost", dream.running ? t("memory.dream.running") : t("memory.dream.now"));
741
+ now.type = "button"; now.disabled = !!dream.running;
742
+ now.onclick = () => { now.disabled = true; now.textContent = t("memory.dream.running"); void act({ action: "dreamNow" }); };
743
+ head.appendChild(now);
744
+ panel.appendChild(el("p", "muted small", dreamNext(dream)));
745
+ if (dream.error) panel.appendChild(el("p", "small memory-error", dream.error));
746
+ if (dream.running) setTimeout(() => { if (shownPanel === "dreaming" && activeId === session) void showDreaming(); }, 3000);
747
+
748
+ const runs = dream.runs || [], log = dream.log || [], proposals = dream.proposals || [];
749
+ const monthRuns = runs.filter((run) => inThisMonth(run.at));
750
+ const monthLog = log.filter((item) => inThisMonth(item.at));
751
+ const metrics = el("div", "dream-metrics");
752
+ const metric = (label, value, sub) => {
753
+ const box = el("div", "dream-metric");
754
+ box.appendChild(el("div", "muted small", label));
755
+ box.appendChild(el("div", "dream-metric-value", value));
756
+ box.appendChild(el("div", "muted small", sub));
757
+ metrics.appendChild(box);
758
+ };
759
+ metric(t("dream.metric.applied"), String(monthLog.length), t("dream.metric.undone", { n: monthLog.filter((item) => item.undone).length }));
760
+ metric(t("dream.metric.waiting"), String(proposals.length), t("dream.metric.suggestions"));
761
+ metric(t("dream.metric.spent"), formatTokens((dream.spend && dream.spend.tokens) || 0), t("dream.metric.cost", { cost: dreamCost(dream.spend && dream.spend.costUsd) }));
762
+ metric(t("dream.metric.runs"), String(monthRuns.length), t("dream.metric.read", { n: monthRuns.reduce((sum, run) => sum + (run.digested || 0), 0) }));
763
+ panel.appendChild(metrics);
764
+ const settings = dream.settings || {};
765
+ panel.appendChild(el("p", "muted small", t("dream.budgetToday", { used: formatTokens((dream.schedule && dream.schedule.spentToday) || 0), limit: formatTokens(settings.dailyTokens || 0) })));
766
+
767
+ const evidence = (row, claims) => {
768
+ if (!claims || !claims.length) return;
769
+ const list = el("ul", "memory-dream-evidence small");
770
+ for (const claim of claims) list.appendChild(el("li", "", t("memory.dream.kind." + claim.kind) + " · " + new Date(claim.at).toLocaleDateString(LOCALE) + " · " + (claim.quote ? "“" + claim.quote + "”" : claim.text)));
771
+ row.appendChild(list);
772
+ };
773
+ const changeRow = (item) => {
774
+ const row = el("div", "dream-change");
775
+ const line = el("div", "dream-line");
776
+ line.appendChild(el("span", "memory-dream-tag " + (item.undone ? "" : "apply"), item.undone ? t("memory.dream.undone") : t("memory.dream.op." + item.op)));
777
+ line.appendChild(el("span", "dream-grow", t("memory.dream.scope." + item.scope) + " · " + item.summary));
778
+ const compare = el("button", "ghost dream-small", t("dream.compare")); compare.type = "button";
779
+ let diff;
780
+ compare.onclick = () => { if (diff) { diff.remove(); diff = undefined; } else { diff = dreamDiff(item); row.appendChild(diff); } };
781
+ line.appendChild(compare);
782
+ if (!item.undone) {
783
+ const undo = el("button", "ghost dream-small", t("memory.dream.undo")); undo.type = "button";
784
+ undo.onclick = () => { undo.disabled = true; void act({ action: "undo", scope: item.scope, id: item.id }); };
785
+ line.appendChild(undo);
786
+ }
787
+ row.appendChild(line);
788
+ evidence(row, item.evidence);
789
+ return row;
790
+ };
791
+
792
+ panel.appendChild(el("h3", "dream-section", t("dream.suggestions", { n: proposals.length })));
793
+ if (!proposals.length) panel.appendChild(el("p", "muted small", t("dream.noSuggestions")));
794
+ for (const item of proposals) {
795
+ const row = el("details", "dream-change");
796
+ const line = el("summary", "dream-line");
797
+ line.appendChild(el("span", "memory-dream-tag review", t("memory.dream.op." + item.op)));
798
+ line.appendChild(el("span", "dream-grow", t("memory.dream.scope." + item.scope) + " · " + (item.summary || (item.targets[0] && item.targets[0].summary) || "")));
799
+ if (item.why) line.appendChild(el("span", "muted small", t("memory.dream.why." + item.why)));
800
+ row.appendChild(line);
801
+ if (item.reason) row.appendChild(el("p", "small", item.reason));
802
+ for (const target of item.targets || []) if (target.body) { row.appendChild(el("div", "muted small", t("memory.dream.before") + " · " + target.summary)); row.appendChild(el("pre", "memory-dream-body before", target.body)); }
803
+ if (item.body) { row.appendChild(el("div", "muted small", t("memory.dream.proposed") + " · " + item.summary + (item.tags && item.tags.length ? " · #" + item.tags.join(" #") : ""))); row.appendChild(el("pre", "memory-dream-body", item.body)); }
804
+ evidence(row, item.evidence);
805
+ const buttons = el("div", "memory-actions");
806
+ if (item.op === "create" || item.op === "update" || item.op === "merge") {
807
+ const apply = el("button", "ghost", t("memory.dream.applyOne")); apply.type = "button";
808
+ apply.onclick = () => { apply.disabled = true; void act({ action: "applyProposal", scope: item.scope, id: item.id }); };
809
+ buttons.appendChild(apply);
810
+ }
811
+ const dismiss = el("button", "ghost", t("memory.dream.dismiss")); dismiss.type = "button";
812
+ dismiss.onclick = () => { void act({ action: "dismissProposal", scope: item.scope, id: item.id }); };
813
+ buttons.appendChild(dismiss);
814
+ row.appendChild(buttons);
815
+ panel.appendChild(row);
816
+ }
817
+
818
+ panel.appendChild(el("h3", "dream-section", t("dream.history")));
819
+ if (!runs.length) panel.appendChild(el("p", "muted small", t("dream.noHistory")));
820
+ for (const run of runs) {
821
+ const row = el("details", "dream-run");
822
+ const line = el("summary", "dream-line");
823
+ line.appendChild(el("span", "dream-when", new Date(run.at).toLocaleString(LOCALE, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })));
824
+ const bad = run.outcome === "refused" || run.outcome === "failed" || run.outcome === "budget";
825
+ line.appendChild(el("span", "memory-dream-tag " + (bad ? "review" : ""), t("dream.trigger." + run.trigger)));
826
+ const counts = t("dream.run.counts", { read: run.digested, applied: run.applied, suggested: run.proposals });
827
+ line.appendChild(el("span", "dream-grow", run.outcome === "no-call" ? t("dream.run.noCall") : bad ? t("dream.run." + run.outcome) + " · " + counts : counts));
828
+ line.appendChild(el("span", "muted small", formatTokens((run.tokens.input || 0) + (run.tokens.output || 0)) + " · " + dreamCost(run.costUsd)));
829
+ row.appendChild(line);
830
+ const changes = log.filter((item) => item.run === run.id);
831
+ if (!changes.length) row.appendChild(el("p", "muted small", t(run.outcome === "refused" ? "dream.run.refusedDetail" : "dream.run.noChanges")));
832
+ for (const item of changes) row.appendChild(changeRow(item));
833
+ for (const error of run.errors || []) row.appendChild(el("p", "small memory-error", error));
834
+ if (run.model) row.appendChild(el("p", "muted small", t("dream.run.model", { model: run.model, seconds: run.seconds })));
835
+ panel.appendChild(row);
836
+ }
837
+ const byHand = log.filter((item) => item.fromProposal);
838
+ if (byHand.length) {
839
+ panel.appendChild(el("h3", "dream-section", t("dream.appliedByYou")));
840
+ for (const item of byHand) panel.appendChild(changeRow(item));
841
+ }
842
+ }
843
+
620
844
  function setNav(active) {
621
845
  // Every nav destination funnels through here, so one call covers them all.
622
846
  track("panel_opened", { panel: active });
@@ -1 +1 @@
1
- {"version":3,"file":"panels.js","sourceRoot":"","sources":["../../../src/page/client/panels.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqpB/B,CAAC"}
1
+ {"version":3,"file":"panels.js","sourceRoot":"","sources":["../../../src/page/client/panels.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAq3B/B,CAAC"}
@@ -400,6 +400,21 @@ async function showSettings(opts) {
400
400
  return box;
401
401
  };
402
402
 
403
+ // A row of small figures inside an opened row: a label, a value, and a line
404
+ // under it. What the row's page is about, at a glance, without opening it.
405
+ const shortWhen = (at) => new Date(at).toLocaleString(LOCALE, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
406
+ const stats = (box, items) => {
407
+ const grid = el("div", "setting-stats");
408
+ for (const item of items) {
409
+ const tile = el("div", "setting-stat" + (item.tone ? " " + item.tone : ""));
410
+ tile.appendChild(el("div", "setting-stat-label", item.label));
411
+ tile.appendChild(el("div", "setting-stat-value", item.value));
412
+ if (item.sub) tile.appendChild(el("div", "setting-stat-sub", item.sub));
413
+ grid.appendChild(tile);
414
+ }
415
+ box.appendChild(grid);
416
+ };
417
+
403
418
  // A labelled block inside an opened row: what the label names, printed as
404
419
  // typed. Used for the permission rules and the settings files, both of
405
420
  // which are lists someone reads once and closes again.
@@ -501,12 +516,60 @@ async function showSettings(opts) {
501
516
  : !result.enabled ? "off"
502
517
  : result.syncing ? "syncing"
503
518
  : (result.errors && result.errors.length) || result.error ? "error" : "on";
504
- box.appendChild(el("div", "small", t("memory.state." + state)));
505
519
  const entries = result.entries || [];
506
520
  const project = entries.filter((entry) => entry.scope !== "user").length;
507
- box.appendChild(el("div", "muted small", result.userAvailable
508
- ? t("memory.countsScoped", { project, user: entries.length - project })
509
- : t("memory.counts", { count: entries.length })));
521
+ const tidy = result.reviewTotal || (result.review || []).length;
522
+ stats(box, [
523
+ { label: t("settings.stat.sync"), value: t("settings.stat.syncState." + state), sub: result.lastSync ? t("settings.stat.syncedAgo", { when: shortWhen(result.lastSync) }) : "", tone: state === "error" || state === "account" ? "warn" : "" },
524
+ { label: t("settings.stat.notes"), value: String(entries.length), sub: result.userAvailable ? t("settings.stat.notesSplit", { project, user: entries.length - project }) : "" },
525
+ { label: t("settings.stat.tidy"), value: String(tidy), sub: t(tidy ? "settings.stat.tidySome" : "settings.stat.tidyNone") },
526
+ ]);
527
+ });
528
+ // Dreaming builds on memory and shows on its page data; its row hides
529
+ // itself when the plugin is off.
530
+ const openDream = el("button", "ghost", t("settings.memoryOpen"));
531
+ openDream.type = "button";
532
+ openDream.addEventListener("click", () => { setNav("dreaming"); void showDreaming(); });
533
+ // The switch is drawn once the state is known; until then, a disabled one.
534
+ // One read draws both the switch and, once the row has been opened, its
535
+ // tiles, so flipping the switch updates what the tiles say too.
536
+ const autoSlot = el("span", "dream-auto-slot");
537
+ autoSlot.appendChild(Object.assign(el("button", "switch"), { type: "button", disabled: true }));
538
+ const dreamRow = field(t("dream.title"), "", t("dream.settingsHint"), autoSlot, openDream);
539
+ let dreamTiles;
540
+ const fillDream = (box, dream) => {
541
+ box.replaceChildren();
542
+ const last = (dream.runs || [])[0];
543
+ const waiting = (dream.proposals || []).length;
544
+ stats(box, [
545
+ { label: t("dream.stat.mode"), value: t(dream.auto ? "dream.stat.modeAuto" : "dream.stat.modeManual"), sub: t(dream.auto ? "dream.stat.modeAutoSub" : "dream.stat.modeManualSub"), tone: dream.auto ? "ok" : "" },
546
+ { label: t("dream.stat.last"), value: last ? shortWhen(last.at) : t("dream.stat.never"), sub: last ? t("dream.stat.lastSub", { applied: last.applied, suggested: last.proposals }) : "" },
547
+ { label: t("dream.stat.month"), value: formatTokens((dream.spend && dream.spend.tokens) || 0) + " tokens", sub: dreamCost(dream.spend && dream.spend.costUsd) },
548
+ ...(waiting ? [{ label: t("dream.stat.waiting"), value: String(waiting), sub: t("dream.metric.suggestions"), tone: "warn" }] : []),
549
+ ]);
550
+ };
551
+ const readDream = async () => {
552
+ const result = await getJson("/plugin-page?id=" + encodeURIComponent(page.id) + "&session=" + encodeURIComponent(activeId || ""));
553
+ const dream = result.sections && result.sections.dreaming;
554
+ // The field helper returns the row body; the row itself is its parent.
555
+ if (!dream) dreamRow.parentNode.hidden = true;
556
+ return dream;
557
+ };
558
+ const drawAuto = async () => {
559
+ const dream = await readDream();
560
+ if (!dream) return;
561
+ autoSlot.replaceChildren(dreamAutoSwitch(dream.auto, async (next) => {
562
+ await api("/plugin-page", { page: page.id, session: activeId, input: { action: "section", section: "dreaming", input: { action: "setAuto", auto: next } } });
563
+ await drawAuto();
564
+ }));
565
+ if (dreamTiles) fillDream(dreamTiles, dream);
566
+ };
567
+ void drawAuto();
568
+ preview(dreamRow, async (box) => {
569
+ const dream = await readDream();
570
+ if (!dream) return void box.replaceChildren();
571
+ dreamTiles = box;
572
+ fillDream(box, dream);
510
573
  });
511
574
  }
512
575
 
@@ -1 +1 @@
1
- {"version":3,"file":"routines.js","sourceRoot":"","sources":["../../../src/page/client/routines.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFA4xCgD,CAAC"}
1
+ {"version":3,"file":"routines.js","sourceRoot":"","sources":["../../../src/page/client/routines.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFA21CgD,CAAC"}
@@ -51,6 +51,7 @@ function repaintPanel() {
51
51
  else if (shownPanel === "audit") void showAudit();
52
52
  else if (shownPanel === "permissions") void showPermissions();
53
53
  else if (shownPanel?.startsWith("nav-plugin-")) void showMemoryPage(shownPanel.slice("nav-plugin-".length));
54
+ else if (shownPanel === "dreaming") void showDreaming();
54
55
  }
55
56
 
56
57
  async function loadProjects() {
@@ -854,7 +855,13 @@ function showUpdatePrompt(prompt) {
854
855
  head.appendChild(el("div", "upd-title", title));
855
856
  dialog.appendChild(head);
856
857
 
857
- if (kind === "download" && prompt.notes) dialog.appendChild(el("div", "upd-notes", prompt.notes));
858
+ // The notes are build/release-notes.md as written, so headings, lists and
859
+ // emphasis render rather than show their markers.
860
+ if (kind === "download" && prompt.notes) {
861
+ const notes = el("div", "upd-notes");
862
+ notes.appendChild(renderMarkdown(prompt.notes));
863
+ dialog.appendChild(notes);
864
+ }
858
865
  const body =
859
866
  kind === "download"
860
867
  ? t("update.prompt.availableBody", vars)
@@ -1 +1 @@
1
- {"version":3,"file":"shell.js","sourceRoot":"","sources":["../../../src/page/client/shell.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA2/BzB,CAAC"}
1
+ {"version":3,"file":"shell.js","sourceRoot":"","sources":["../../../src/page/client/shell.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAkgCzB,CAAC"}
@@ -578,8 +578,18 @@ async function deleteProject(cwd, name) {
578
578
  let sessionRows = [];
579
579
 
580
580
  async function loadSessions() {
581
+ const revision = sidebarTaskRevision;
582
+ const request = ++sessionListRequest;
581
583
  const body = await getJson("/sessions");
584
+ if (request !== sessionListRequest) return;
585
+ if (body.error) return void addNotice(body.error, "err");
582
586
  sessionRows = body.sessions || [];
587
+ for (const s of sessionRows) {
588
+ const cached = sidebarTasks.get(s.id);
589
+ if (!cached || cached.revision <= revision) {
590
+ sidebarTasks.set(s.id, { tasks: s.tasks || [], revision });
591
+ }
592
+ }
583
593
  sessionOrder = body.order || {};
584
594
  paintSessions();
585
595
  }
@@ -707,7 +717,7 @@ function paintSessions() {
707
717
  if (open) {
708
718
  const all = showingAll.has(ROUTINE_GROUP) || scheduled.length <= PREVIEW + 1;
709
719
  for (const s of all ? scheduled : scheduled.slice(0, PREVIEW)) {
710
- sessionList.appendChild(sessionRow(s, true));
720
+ sessionList.appendChild(sessionBlock(s, true));
711
721
  }
712
722
  if (!all) sessionList.appendChild(showMore(ROUTINE_GROUP, true));
713
723
  }
@@ -730,7 +740,7 @@ function paintSessions() {
730
740
  const shown = all
731
741
  ? list
732
742
  : list.filter((s, i) => i < PREVIEW || s.pinned);
733
- for (const s of shown) sessionList.appendChild(sessionRow(s, named));
743
+ for (const s of shown) sessionList.appendChild(sessionBlock(s, named));
734
744
  if (!all) sessionList.appendChild(showMore(cwd, named));
735
745
  }
736
746
  }
@@ -1210,6 +1220,9 @@ async function deleteSession(id) {
1210
1220
  // leaves them pointing at nothing. Said once, rather than blocked.
1211
1221
  if (body.forks) addNotice(t("session.deletedForks", { n: body.forks }), "notice");
1212
1222
 
1223
+ sidebarTasks.delete(id);
1224
+ delete taskTreeOpen[id];
1225
+ saveTaskTreeOpen();
1213
1226
  sessionRows = sessionRows.filter((s) => s.id !== id);
1214
1227
  paintSessions();
1215
1228
  await loadSessions();
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/page/client/state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8/C9B,CAAC"}
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/page/client/state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2gD9B,CAAC"}