@songsid/agend 2.3.4-beta.8 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +5 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex-metadata.d.ts +14 -0
- package/dist/backend/codex-metadata.js +45 -0
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex-speed.d.ts +14 -0
- package/dist/backend/codex-speed.js +35 -0
- package/dist/backend/codex-speed.js.map +1 -0
- package/dist/backend/codex.js +17 -5
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.js +11 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/cache-ledger.d.ts +95 -0
- package/dist/cache-ledger.js +276 -4
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +235 -17
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/config-validator.js +5 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +11 -4
- package/dist/daemon.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +93 -4
- package/dist/fleet-manager.js +451 -39
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +3 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +7 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-config-impact.js +2 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +6 -2
- package/dist/instance-lifecycle.js +11 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +33 -1
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +28 -0
- package/dist/log-service.js +29 -0
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +43 -1
- package/dist/logger.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/needs-you-hub.d.ts +6 -1
- package/dist/needs-you-hub.js +30 -7
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +16 -2
- package/dist/needs-you.js +9 -1
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +3 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +5 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +22 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +16 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-instance-patch.js +27 -0
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-md-strings.js +9 -0
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +53 -3
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +5 -2
- package/dist/ui/panel-needs.js +13 -5
- package/dist/ui/preview-panel.js +78 -2
- package/dist/ui/settings-codex-copy.js +19 -0
- package/dist/ui/settings-dialogs.js +8 -0
- package/dist/ui/settings-model.js +16 -2
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app.css +19 -0
- package/dist/ui/shared/panel-view.js +77 -4
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +16 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +6 -1
- package/dist/usage-rollup.js +16 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +16 -0
- package/dist/view-api.js +100 -16
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +8 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/launcher/private-home.cjs +1 -1
- package/package.json +1 -1
|
@@ -282,6 +282,13 @@ const REDRAWS_IN_PLACE = new Set(["grok"]);
|
|
|
282
282
|
const FULL_SCREEN = new Set(["claude-code", "opencode", "grok"]); // agy: its chat UI unverified — found by the read
|
|
283
283
|
/** The full-screen CLIs whose earlier output the Log tab shows (it reads their transcript: log-service.ts). */
|
|
284
284
|
const LOG_HAS_HISTORY = new Set(["claude-code"]);
|
|
285
|
+
/**
|
|
286
|
+
* Claude history in the View (view-history.js): the full-screen CLIs whose View shows their earlier conversation from
|
|
287
|
+
* their transcript, above the live screen. A server without it (an older one) leaves them as before: no scrollback.
|
|
288
|
+
*/
|
|
289
|
+
const TRANSCRIPT_HISTORY = new Set(["claude-code"]);
|
|
290
|
+
/** How near the top (px) a history being read asks for the page before it. */
|
|
291
|
+
const OLDER_AT_PX = 48;
|
|
285
292
|
/**
|
|
286
293
|
* M-2267 option (2), the leader's safe default: where a full-screen CLI's earlier output is — its Log tab (or that the
|
|
287
294
|
* Log tab is off, and where to turn it on). Only for whom the Log tab is (a signed-in session on the full app; never a
|
|
@@ -304,8 +311,11 @@ function Terminal({ name, lease, density, every = 800, backend = "", signedIn =
|
|
|
304
311
|
const [newBelow, setNewBelow] = useState(false);
|
|
305
312
|
const [historyNote, setHistoryNoteState] = useState(null);
|
|
306
313
|
// A CLI found to keep no scrollback (full screen): nothing is offered or read again for it; the note says why, briefly.
|
|
307
|
-
const
|
|
308
|
-
const
|
|
314
|
+
const keepsNone = FULL_SCREEN.has(backend) && !TRANSCRIPT_HISTORY.has(backend);
|
|
315
|
+
const [noScrollback, setNoScrollback] = useState(keepsNone);
|
|
316
|
+
const noScrollbackRef = useRef(keepsNone);
|
|
317
|
+
// Claude history in the View: the history shown while reading (view-history.js's createHistory), or null.
|
|
318
|
+
const transcript = useRef(null);
|
|
309
319
|
const logLink = logLinkFor(name, backend, signedIn);
|
|
310
320
|
const noteTimer = useRef(null);
|
|
311
321
|
const setHistoryNote = (note, brief = false) => {
|
|
@@ -402,6 +412,7 @@ function Terminal({ name, lease, density, every = 800, backend = "", signedIn =
|
|
|
402
412
|
// The person asked for what scrolled off: one bounded read, then reading (or why there is nothing to read).
|
|
403
413
|
const loadEarlier = useRef(null);
|
|
404
414
|
const follow = useRef(null);
|
|
415
|
+
const olderPage = useRef(null);
|
|
405
416
|
useEffect(() => {
|
|
406
417
|
const reads = readStream(lease);
|
|
407
418
|
const refresh = async () => {
|
|
@@ -439,6 +450,7 @@ function Terminal({ name, lease, density, every = 800, backend = "", signedIn =
|
|
|
439
450
|
const from = modeRef.current;
|
|
440
451
|
if (from !== "follow" && !(from === "reading" && !screenAt.current.history)) return;
|
|
441
452
|
if (noScrollbackRef.current) { setHistoryNote(tn("noEarlier"), true); return; } // known: nothing to read
|
|
453
|
+
if (TRANSCRIPT_HISTORY.has(backend)) { await loadTranscript(from); return; }
|
|
442
454
|
const box0 = term.current;
|
|
443
455
|
// Where the person is, measured from the end: the screen's lines stay where they are under the scrollback added.
|
|
444
456
|
const fromEnd = box0 ? box0.scrollHeight - box0.scrollTop : 0;
|
|
@@ -477,8 +489,65 @@ function Terminal({ name, lease, density, every = 800, backend = "", signedIn =
|
|
|
477
489
|
}
|
|
478
490
|
} catch { if (lease.current()) { back(); setHistoryNote(tn("earlierFailed"), true); } }
|
|
479
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* Claude history in the View: the conversation before the live screen, from the transcript (the renderer is its own
|
|
494
|
+
* lazy module), drawn above the screen as it is now — then reading, as for a scrollback. Older pages come as the
|
|
495
|
+
* person nears the top. A server without it: this CLI keeps no scrollback, as before.
|
|
496
|
+
*/
|
|
497
|
+
const loadTranscript = async (from) => {
|
|
498
|
+
const back = () => { setMode(from); if (from === "follow") toBottom(); };
|
|
499
|
+
const screen = screenAt.current.drawn == null ? "" : screenAt.current.drawn;
|
|
500
|
+
setMode("loading"); setHistoryNote(tn("loadingEarlier"));
|
|
501
|
+
try {
|
|
502
|
+
const mod = await import("./view-history.js");
|
|
503
|
+
if (!lease.current() || modeRef.current !== "loading" || !pre.current) return;
|
|
504
|
+
let h = null;
|
|
505
|
+
h = mod.createHistory({ name, fetch: (u) => lease.fetch(u), alive: () => lease.current() && transcript.current === h,
|
|
506
|
+
doc: document, font: getComputedStyle(pre.current).fontFamily, cols: grid.current.cols });
|
|
507
|
+
transcript.current = h;
|
|
508
|
+
const got = await h.first(screen);
|
|
509
|
+
if (!lease.current() || modeRef.current !== "loading" || !pre.current || transcript.current !== h) return;
|
|
510
|
+
if (got.kind !== "ok") {
|
|
511
|
+
transcript.current = null;
|
|
512
|
+
if (got.kind === "unsupported") { noScrollbackRef.current = true; setNoScrollback(true); back(); setHistoryNote(tn("noEarlier"), true); return; }
|
|
513
|
+
back();
|
|
514
|
+
setHistoryNote(got.kind === "empty" ? tn("nothingEarlier") : got.status === 429 ? null : tn("earlierFailed"), true);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
screenAt.current.reading = screen; screenAt.current.history = true;
|
|
518
|
+
setNewBelow(screenAt.current.latest != null && !sameScreen(screen, screenAt.current.latest));
|
|
519
|
+
setMode("reading"); setHistoryNote(null);
|
|
520
|
+
draw(screen); // the screen as it is (the one frame write), then the history above it
|
|
521
|
+
pre.current.insertBefore(h.root, pre.current.firstChild);
|
|
522
|
+
fit();
|
|
523
|
+
const box = term.current;
|
|
524
|
+
if (box) {
|
|
525
|
+
const px = parseFloat(getComputedStyle(pre.current).fontSize);
|
|
526
|
+
const lineH = Number.isFinite(px) && px > 0 ? px * 1.2 : 16;
|
|
527
|
+
// The history's last lines and the divider right above the screen.
|
|
528
|
+
box.scrollTop = Math.max(0, box.scrollHeight - box.clientHeight - lineH * 3);
|
|
529
|
+
}
|
|
530
|
+
} catch { if (lease.current() && modeRef.current === "loading") { transcript.current = null; back(); setHistoryNote(tn("earlierFailed"), true); } }
|
|
531
|
+
};
|
|
532
|
+
/** The page before the history shown, above it — where the person is stays where it is. */
|
|
533
|
+
let olderBusy = false;
|
|
534
|
+
olderPage.current = async () => {
|
|
535
|
+
const h = transcript.current, box = term.current;
|
|
536
|
+
if (olderBusy || !h || !box || modeRef.current !== "reading" || !screenAt.current.history) return;
|
|
537
|
+
olderBusy = true;
|
|
538
|
+
const fromEnd = box.scrollHeight - box.scrollTop;
|
|
539
|
+
const capped = !h.hasOlder();
|
|
540
|
+
if (!capped) setHistoryNote(tn("loadingEarlier"));
|
|
541
|
+
try {
|
|
542
|
+
const got = await h.older();
|
|
543
|
+
if (!lease.current() || transcript.current !== h) return;
|
|
544
|
+
box.scrollTop = Math.max(0, box.scrollHeight - fromEnd);
|
|
545
|
+
if (!capped) setHistoryNote(got.kind === "failed" ? tn("earlierFailed") : null, got.kind === "failed");
|
|
546
|
+
} finally { olderBusy = false; }
|
|
547
|
+
};
|
|
480
548
|
follow.current = () => {
|
|
481
549
|
if (modeRef.current === "follow") return;
|
|
550
|
+
transcript.current = null;
|
|
482
551
|
setMode("follow"); setNewBelow(false); setHistoryNote(null); screenAt.current.history = false;
|
|
483
552
|
const latest = screenAt.current.latest;
|
|
484
553
|
if (latest != null && pre.current) { draw(latest); fit(); }
|
|
@@ -501,7 +570,9 @@ function Terminal({ name, lease, density, every = 800, backend = "", signedIn =
|
|
|
501
570
|
// Asking for the earlier output: from following, or from reading the screen as it was — at the top.
|
|
502
571
|
const mayLoad = () => (modeRef.current === "follow" || (modeRef.current === "reading" && !screenAt.current.history)) && box.scrollTop <= 0;
|
|
503
572
|
const gesture = () => { gestureAt.current = performance.now(); };
|
|
504
|
-
|
|
573
|
+
// Claude's history being read, at (or near) its top: the page before it.
|
|
574
|
+
const nearTop = () => modeRef.current === "reading" && screenAt.current.history && transcript.current && box.scrollTop <= OLDER_AT_PX;
|
|
575
|
+
lease.on(box, "wheel", (e) => { gesture(); if (e.deltaY < 0 && mayLoad()) loadEarlier.current(); else if (e.deltaY < 0 && nearTop()) olderPage.current(); }, { passive: true });
|
|
505
576
|
let touchY = null;
|
|
506
577
|
lease.on(box, "touchstart", (e) => { gesture(); touchY = e.touches && e.touches[0] ? e.touches[0].clientY : null; }, { passive: true });
|
|
507
578
|
lease.on(box, "touchmove", (e) => {
|
|
@@ -509,15 +580,17 @@ function Terminal({ name, lease, density, every = 800, backend = "", signedIn =
|
|
|
509
580
|
const y = e.touches && e.touches[0] ? e.touches[0].clientY : null;
|
|
510
581
|
if (touchY == null || y == null) return;
|
|
511
582
|
if (y - touchY > 24 && mayLoad()) { touchY = null; loadEarlier.current(); }
|
|
583
|
+
else if (y - touchY > 24 && nearTop()) { touchY = null; olderPage.current(); }
|
|
512
584
|
}, { passive: true });
|
|
513
585
|
lease.on(box, "pointerdown", gesture, { passive: true }); // the scrollbar, dragged
|
|
514
586
|
lease.on(box, "keydown", (e) => {
|
|
515
587
|
gesture();
|
|
516
588
|
if ((e.key === "PageUp" || e.key === "ArrowUp" || e.key === "Home") && mayLoad()) { e.preventDefault(); loadEarlier.current(); }
|
|
589
|
+
else if ((e.key === "PageUp" || e.key === "ArrowUp" || e.key === "Home") && nearTop()) olderPage.current();
|
|
517
590
|
else if (modeRef.current === "reading" && e.key === "End") { e.preventDefault(); follow.current(); }
|
|
518
591
|
});
|
|
519
592
|
lease.on(box, "scroll", () => {
|
|
520
|
-
if (modeRef.current === "reading") { if (atBottom()) follow.current(); return; }
|
|
593
|
+
if (modeRef.current === "reading") { if (atBottom()) follow.current(); else if (nearTop()) olderPage.current(); return; }
|
|
521
594
|
// #2969 review (Prism P2): a screen taller than its box (a larger Aa), moved up from the bottom by the person:
|
|
522
595
|
// reading what is on it — the refresh no longer pulls them back down.
|
|
523
596
|
if (modeRef.current === "follow" && recentGesture() && !atBottom()) startReading(screenAt.current.drawn, false);
|
|
@@ -64,6 +64,22 @@ function expiryText(iso) {
|
|
|
64
64
|
if (!(ms > 0)) return "";
|
|
65
65
|
return usageText("", { key: "usage.ticket_expiry", args: [`${at.getMonth() + 1}/${at.getDate()}`, usageDuration(ms)] });
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* #3066 §1: a server line ({ text, i18n }) in this page's language — or, when this tab's strings predate the key (a
|
|
69
|
+
* stale tab), the server's own English text, never the bare key.
|
|
70
|
+
*/
|
|
71
|
+
export function capacityLine(x) {
|
|
72
|
+
if (!x || typeof x.text !== "string") return "";
|
|
73
|
+
const key = x.i18n && typeof x.i18n.key === "string" ? `view.${x.i18n.key}` : null;
|
|
74
|
+
return key && t(key) !== key ? usageText(x.text, x.i18n) : x.text;
|
|
75
|
+
}
|
|
76
|
+
/** #3066 §1: a weekly quota's measured 100% (or why not yet), and the limit it always carries: only this fleet counts. */
|
|
77
|
+
function UsageCapacity({ c }) {
|
|
78
|
+
const notes = Array.isArray(c.notes) ? c.notes : [];
|
|
79
|
+
return html`<div class=${`u-cap${c.status === "ok" ? "" : " u-cap-wait"}`}>
|
|
80
|
+
<div class="u-cap-line">${capacityLine(c.line)}</div>
|
|
81
|
+
${notes.map((n, i) => html`<div key=${i} class="u-cap-note">${capacityLine(n)}</div>`)}</div>`;
|
|
82
|
+
}
|
|
67
83
|
export function UsageMetric({ m }) {
|
|
68
84
|
const bar = useRef(null);
|
|
69
85
|
const label = usageText(m.label, m.labelI18n);
|
|
@@ -78,7 +94,8 @@ export function UsageMetric({ m }) {
|
|
|
78
94
|
const sub = [note, resetText(m.resetsAt)].filter(Boolean).join(" · ");
|
|
79
95
|
return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span>${word ? html`<span class=${`u-status ${cls}`}>${word}</span>` : null}
|
|
80
96
|
<span class=${`u-pct ${cls}`}>${pct.toFixed(0)}<span class="u-unit">%</span></span></div>
|
|
81
|
-
<div class="u-meter"><div ref=${bar} class=${`u-fill ${cls}`}></div></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}
|
|
97
|
+
<div class="u-meter"><div ref=${bar} class=${`u-fill ${cls}`}></div></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}
|
|
98
|
+
${m.capacity ? html`<${UsageCapacity} c=${m.capacity} />` : null}</div>`;
|
|
82
99
|
}
|
|
83
100
|
if (m.type === "dollars") {
|
|
84
101
|
const val = m.limit ? `$${(m.used ?? 0).toFixed(2)} / $${m.limit.toFixed(2)}` : `$${(m.used ?? 0).toFixed(2)}`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* Claude history in the View (view-history.js): its own stylesheet, linked by the module itself, so a tab opened before
|
|
2
|
+
an upgrade (an app.css without these rules) draws it the same. Colours come from the live screen, set per element. */
|
|
3
|
+
.v-pre .vh { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
4
|
+
.vh-line { padding-left: 2ch; text-indent: -2ch; min-height: 1.2em; }
|
|
5
|
+
.vh-line.vh-res { padding-left: 5ch; text-indent: -5ch; }
|
|
6
|
+
.vh-step + .vh-step { margin-top: 1.2em; }
|
|
7
|
+
.vh-b { font-weight: 700; }
|
|
8
|
+
.vh-diff { display: inline-block; width: 100%; vertical-align: top; text-indent: 0; }
|
|
9
|
+
.vh-dim { opacity: .6; }
|
|
10
|
+
.vh-more { background: none; border: 0; padding: 0; margin: 0; font: inherit; text-indent: 0; cursor: pointer;
|
|
11
|
+
text-decoration: underline dotted; text-underline-offset: 2px; }
|
|
12
|
+
.vh-more:disabled { cursor: progress; opacity: .7; }
|
|
13
|
+
.vh-more:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; }
|
|
14
|
+
.vh-divider, .vh-cap { font-family: var(--font-ui, sans-serif); font-size: .85em; color: #8a8a8a; }
|
|
15
|
+
.vh-divider { display: flex; align-items: center; gap: 1ch; margin: .8em 0 .6em; white-space: nowrap; }
|
|
16
|
+
.vh-divider::before, .vh-divider::after { content: ""; flex: 1; border-top: 1px dashed #3c3c3c; }
|
|
17
|
+
.vh-cap { white-space: normal; padding-bottom: .8em; }
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Claude history in the View (the user, hmsg-a8947a2c / hmsg-b4c7ccdf; leader cid-claude-history-build). Claude Code
|
|
2
|
+
// draws on the alternate screen, so tmux keeps nothing above it: scrolling up in its View shows the conversation's
|
|
3
|
+
// earlier part from its transcript (GET /api/pane/<name>?transcript=1, view-history.ts), drawn like Claude's own
|
|
4
|
+
// Ctrl+O view — "❯" prompts on their band, "●" replies and tool calls, "⎿" results, diffs, long output folded — directly
|
|
5
|
+
// above the live screen, under a faint divider.
|
|
6
|
+
//
|
|
7
|
+
// The look is sampled from the live screen the View already shows (samplePalette): the prompt band, the bullet and
|
|
8
|
+
// result glyphs, the tool and diff colours, as this Claude version draws them; Claude 2.1.318's own values when the
|
|
9
|
+
// screen has none of them. A glyph the page's fonts lack falls back (⏺ → ●, ⎿ → └, ❯ → >).
|
|
10
|
+
//
|
|
11
|
+
// Loaded on first use by panel-view.js (a lazy module of its own). A tab opened before an upgrade runs it with the old
|
|
12
|
+
// shell, so it imports nothing but app-i18n.js's register/t (in every shell since #1408), registers its own words and
|
|
13
|
+
// links its own stylesheet (/assets/view-history.css): nothing it needs lives in a long-lived module. A server without
|
|
14
|
+
// the history (an older one answers the pane's text) makes `first()` say `unsupported`: the View stays as it was.
|
|
15
|
+
import { register, t } from "./app-i18n.js";
|
|
16
|
+
|
|
17
|
+
register("viewHistory", {
|
|
18
|
+
en: {
|
|
19
|
+
divider: "History above",
|
|
20
|
+
more: "… +{0} lines (click to expand)",
|
|
21
|
+
moreFailed: "Could not load the full text.",
|
|
22
|
+
cut: "… {0} more characters not shown",
|
|
23
|
+
capped: "Only the latest {0} steps are shown here.",
|
|
24
|
+
},
|
|
25
|
+
"zh-TW": {
|
|
26
|
+
divider: "以上是對話紀錄",
|
|
27
|
+
more: "… 還有 {0} 行(點一下展開)",
|
|
28
|
+
moreFailed: "無法載入完整內容。",
|
|
29
|
+
cut: "… 還有 {0} 個字元未顯示",
|
|
30
|
+
capped: "這裡只顯示最近 {0} 則紀錄。",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const w = (key, ...values) => t(`viewHistory.${key}`, ...values);
|
|
34
|
+
|
|
35
|
+
/** About this many steps are kept on the page; older pages are not read past it. */
|
|
36
|
+
export const HISTORY_STEP_CAP = 1000;
|
|
37
|
+
|
|
38
|
+
// ── The palette, sampled from the live screen ──
|
|
39
|
+
const BASE = ["#000000", "#cd3131", "#0dbc79", "#e5e510", "#2472c8", "#bc3fbc", "#11a8cd", "#e5e5e5"];
|
|
40
|
+
const BRIGHT = ["#666666", "#f14c4c", "#23d18b", "#f5f543", "#3b8eea", "#d670d6", "#29b8db", "#ffffff"];
|
|
41
|
+
function xterm256(n) {
|
|
42
|
+
if (n < 8) return BASE[n];
|
|
43
|
+
if (n < 16) return BRIGHT[n - 8];
|
|
44
|
+
if (n < 232) { n -= 16; const r = Math.floor(n / 36), g = Math.floor((n % 36) / 6), b = n % 6; const v = (x) => (x ? x * 40 + 55 : 0); return `rgb(${v(r)},${v(g)},${v(b)})`; }
|
|
45
|
+
const l = (n - 232) * 10 + 8; return `rgb(${l},${l},${l})`;
|
|
46
|
+
}
|
|
47
|
+
/** Claude Code 2.1.318's own look (captured from a real pane, dark theme): what is used when the screen shows none. */
|
|
48
|
+
export const DEFAULT_PALETTE = Object.freeze({
|
|
49
|
+
promptGlyph: "❯", promptBand: xterm256(237), promptFg: xterm256(246), promptText: xterm256(231),
|
|
50
|
+
bullet: "●", replyFg: xterm256(231), toolFg: xterm256(114), failedFg: xterm256(203), runningFg: xterm256(246),
|
|
51
|
+
resultGlyph: "⎿", resultFg: xterm256(246),
|
|
52
|
+
delFg: xterm256(167), delBg: xterm256(52), addFg: xterm256(77), addBg: xterm256(22), errorFg: xterm256(203),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** The screen's text as lines of runs: { text, fg, bg, bold } (colours as CSS, null when unset). */
|
|
56
|
+
export function ansiRuns(text) {
|
|
57
|
+
const lines = [[]];
|
|
58
|
+
let fg = null, bg = null, bold = false, reverse = false;
|
|
59
|
+
const push = (chunk) => {
|
|
60
|
+
const parts = chunk.split("\n");
|
|
61
|
+
parts.forEach((part, i) => {
|
|
62
|
+
if (i > 0) lines.push([]);
|
|
63
|
+
if (!part) return;
|
|
64
|
+
let f = typeof fg === "number" ? (bold && fg < 8 ? BRIGHT[fg] : BASE[fg]) : fg;
|
|
65
|
+
let b = typeof bg === "number" ? BASE[bg] : bg;
|
|
66
|
+
if (reverse) [f, b] = [b, f];
|
|
67
|
+
lines[lines.length - 1].push({ text: part, fg: f, bg: b, bold });
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const re = /\x1b\[([0-9;]*)m/g;
|
|
71
|
+
let last = 0, m;
|
|
72
|
+
while ((m = re.exec(text)) !== null) {
|
|
73
|
+
push(text.slice(last, m.index));
|
|
74
|
+
last = re.lastIndex;
|
|
75
|
+
const codes = m[1].split(";").filter((x) => x !== "").map(Number);
|
|
76
|
+
if (codes.length === 0) codes.push(0);
|
|
77
|
+
for (let i = 0; i < codes.length; i++) {
|
|
78
|
+
const c = codes[i];
|
|
79
|
+
if (c === 0) { fg = bg = null; bold = reverse = false; }
|
|
80
|
+
else if (c === 1) bold = true;
|
|
81
|
+
else if (c === 22) bold = false;
|
|
82
|
+
else if (c === 7) reverse = true;
|
|
83
|
+
else if (c === 27) reverse = false;
|
|
84
|
+
else if (c === 39) fg = null;
|
|
85
|
+
else if (c === 49) bg = null;
|
|
86
|
+
else if (c >= 30 && c <= 37) fg = c - 30;
|
|
87
|
+
else if (c >= 90 && c <= 97) fg = BRIGHT[c - 90];
|
|
88
|
+
else if (c >= 40 && c <= 47) bg = c - 40;
|
|
89
|
+
else if (c >= 100 && c <= 107) bg = BRIGHT[c - 100];
|
|
90
|
+
else if (c === 38 || c === 48) {
|
|
91
|
+
let col = null;
|
|
92
|
+
if (codes[i + 1] === 5) { col = xterm256(codes[i + 2]); i += 2; }
|
|
93
|
+
else if (codes[i + 1] === 2) { col = `rgb(${codes[i + 2] || 0},${codes[i + 3] || 0},${codes[i + 4] || 0})`; i += 4; }
|
|
94
|
+
if (c === 38) fg = col; else bg = col;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
push(text.slice(last));
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const PROMPT_GLYPHS = "❯>›";
|
|
103
|
+
const BULLETS = "●⏺";
|
|
104
|
+
const RESULT_GLYPHS = "⎿└";
|
|
105
|
+
/**
|
|
106
|
+
* How this Claude draws its conversation, read off the live screen (ANSI, as the View fetched it): each value found
|
|
107
|
+
* replaces the default; the most frequent wins where a screen shows several (a failed tool's red bullet among green).
|
|
108
|
+
*/
|
|
109
|
+
export function samplePalette(screen) { return sampleScreen(screen).palette; }
|
|
110
|
+
/** samplePalette, and which of its values the screen gave (the rest are the defaults) — for the version audit. */
|
|
111
|
+
export function sampleScreen(screen) {
|
|
112
|
+
const p = { ...DEFAULT_PALETTE };
|
|
113
|
+
const found = new Set();
|
|
114
|
+
const set = (k, v) => { if (v) { p[k] = v; found.add(k); } };
|
|
115
|
+
if (typeof screen !== "string" || !screen) return { palette: p, found: [] };
|
|
116
|
+
const tally = { toolFg: new Map(), replyFg: new Map() };
|
|
117
|
+
const count = (map, v) => { if (v) map.set(v, (map.get(v) || 0) + 1); };
|
|
118
|
+
let banded = false, glyphSeen = false;
|
|
119
|
+
for (const runs of ansiRuns(screen)) {
|
|
120
|
+
const textOf = runs.map((r) => r.text).join("");
|
|
121
|
+
const lead = textOf.length - textOf.trimStart().length;
|
|
122
|
+
// Find the run holding column `col` (and the offset in it).
|
|
123
|
+
const at = (col) => { let n = 0; for (const r of runs) { if (col < n + r.text.length) return r; n += r.text.length; } return null; };
|
|
124
|
+
const first = textOf.charAt(lead);
|
|
125
|
+
// A prompt: its glyph then a space, on a band (a past prompt) — or bare (the composer: the glyph only).
|
|
126
|
+
if (PROMPT_GLYPHS.includes(first) && textOf.charAt(lead + 1) === " " && !banded) {
|
|
127
|
+
const g = at(lead);
|
|
128
|
+
if (g && g.bg) {
|
|
129
|
+
banded = true; glyphSeen = true;
|
|
130
|
+
set("promptGlyph", first); set("promptBand", g.bg); set("promptFg", g.fg);
|
|
131
|
+
const rest = runs.slice(runs.indexOf(g) + 1).find((r) => r.text.trim());
|
|
132
|
+
if (rest) set("promptText", rest.fg);
|
|
133
|
+
} else if (!glyphSeen) { glyphSeen = true; set("promptGlyph", first); }
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// A bullet at the line's start: a tool call ("● Name(" with the name bold) or a reply.
|
|
137
|
+
if (lead === 0 && BULLETS.includes(first) && textOf.charAt(1) === " ") {
|
|
138
|
+
set("bullet", first);
|
|
139
|
+
const g = at(0), next = at(2);
|
|
140
|
+
const tool = !!(next && next.bold) && /^. [^(]+\(/.test(textOf);
|
|
141
|
+
count(tool ? tally.toolFg : tally.replyFg, g && g.fg);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// A result: two spaces, its glyph.
|
|
145
|
+
if (lead === 2 && RESULT_GLYPHS.includes(first)) {
|
|
146
|
+
set("resultGlyph", first);
|
|
147
|
+
const g = at(2); if (g) set("resultFg", g.fg);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// A diff row: a line number then - or +, on a coloured background.
|
|
151
|
+
const row = /^\s+\d+\s?([-+])/.exec(textOf);
|
|
152
|
+
if (row) {
|
|
153
|
+
const g = at(textOf.indexOf(row[1], lead));
|
|
154
|
+
if (g && g.bg) { if (row[1] === "-") { set("delBg", g.bg); set("delFg", g.fg); } else { set("addBg", g.bg); set("addFg", g.fg); } }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const top = (map) => { let best = null, n = 0; for (const [k, v] of map) if (v > n) { best = k; n = v; } return best; };
|
|
158
|
+
set("toolFg", top(tally.toolFg));
|
|
159
|
+
set("replyFg", top(tally.replyFg));
|
|
160
|
+
return { palette: p, found: [...found] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Glyphs the page's fonts lack ──
|
|
164
|
+
const FALLBACK = { "⏺": "●", "●": "*", "⎿": "└", "└": "|", "❯": ">", "›": ">" };
|
|
165
|
+
const glyphCache = new Map();
|
|
166
|
+
/**
|
|
167
|
+
* Whether `ch` draws as itself in `font` (a CSS font-family list): drawn next to a code point no font has, the two must
|
|
168
|
+
* differ. No canvas (a test page): assumed drawn.
|
|
169
|
+
*/
|
|
170
|
+
export function hasGlyph(ch, font, doc = typeof document !== "undefined" ? document : null) {
|
|
171
|
+
const key = `${ch}\u0000${font}`;
|
|
172
|
+
if (glyphCache.has(key)) return glyphCache.get(key);
|
|
173
|
+
let ok = true;
|
|
174
|
+
try {
|
|
175
|
+
const canvas = doc && doc.createElement ? doc.createElement("canvas") : null;
|
|
176
|
+
const ctx = canvas && typeof canvas.getContext === "function" ? canvas.getContext("2d") : null;
|
|
177
|
+
if (ctx && typeof ctx.getImageData === "function") {
|
|
178
|
+
canvas.width = 40; canvas.height = 40;
|
|
179
|
+
const draw = (c) => { ctx.clearRect(0, 0, 40, 40); ctx.font = `32px ${font}`; ctx.fillStyle = "#000"; ctx.textBaseline = "top"; ctx.fillText(c, 2, 2); return ctx.getImageData(0, 0, 40, 40).data; };
|
|
180
|
+
const a = draw(ch), none = draw("\u{10FFFD}");
|
|
181
|
+
ok = false;
|
|
182
|
+
for (let i = 3; i < a.length; i += 4) if (a[i] !== none[i]) { ok = true; break; }
|
|
183
|
+
}
|
|
184
|
+
} catch { ok = true; }
|
|
185
|
+
glyphCache.set(key, ok);
|
|
186
|
+
return ok;
|
|
187
|
+
}
|
|
188
|
+
/** The glyph as drawn on this page: itself, or its fallback when the fonts lack it. */
|
|
189
|
+
export function drawable(ch, font, doc) {
|
|
190
|
+
let g = ch;
|
|
191
|
+
for (let i = 0; i < 3 && g && !hasGlyph(g, font, doc); i++) g = FALLBACK[g] || g;
|
|
192
|
+
return g;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Which steps the live screen already shows ──
|
|
196
|
+
// Text a needle is compared by: no whitespace (a wrap moves it) and no markdown marks (Claude draws them away).
|
|
197
|
+
const squash = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "").replace(/[\s*_`#>│]/g, "");
|
|
198
|
+
/**
|
|
199
|
+
* What of a prompt or a reply is looked for on the screen: its WHOLE text, normalised (#3069 review r3, Prism P2-2 — a
|
|
200
|
+
* fixed prefix, however long, is no proof: it can occur inside another message). None for a tool: Claude may draw it
|
|
201
|
+
* summarised.
|
|
202
|
+
*/
|
|
203
|
+
function needle(step) {
|
|
204
|
+
if (step.kind !== "user" && step.kind !== "assistant") return null;
|
|
205
|
+
const s = squash(step.body || "");
|
|
206
|
+
return s.length >= 4 ? s : null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The steps before the live screen, matched conservatively (#3069 review, Prism P2-2): walking back from the newest,
|
|
210
|
+
* each prompt and reply must be found WHOLE on the screen, BEFORE the place the newer one was found — one occurrence
|
|
211
|
+
* never proves two messages, and a shared opening is never taken for the same message. The first one not found that
|
|
212
|
+
* way (a message only partly shown included), and everything older, is history. Tool calls go with their neighbours. Unsure means shown again: a
|
|
213
|
+
* step the screen shows may come twice — never one it does not show not at all.
|
|
214
|
+
*/
|
|
215
|
+
export function beforeScreen(steps, screen) {
|
|
216
|
+
const text = squash(screen || "");
|
|
217
|
+
let cut = steps.length, limit = text.length;
|
|
218
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
219
|
+
const n = needle(steps[i]);
|
|
220
|
+
if (n === null) continue;
|
|
221
|
+
const at = limit - n.length >= 0 ? text.lastIndexOf(n, limit - n.length) : -1;
|
|
222
|
+
if (at < 0) break;
|
|
223
|
+
limit = at;
|
|
224
|
+
cut = i;
|
|
225
|
+
}
|
|
226
|
+
return steps.slice(0, cut);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Drawing ──
|
|
230
|
+
const TOOL_WORDS = { Edit: "Update", MultiEdit: "Update", Grep: "Search", Glob: "Search", WebFetch: "Fetch", WebSearch: "Web Search", TodoWrite: "Update Todos", Agent: "Task", NotebookEdit: "Edit Notebook" };
|
|
231
|
+
/** Claude's word for a tool, as its screen shows it ("Update" for Edit; "server - tool (MCP)" for an MCP tool). */
|
|
232
|
+
export function toolWord(tool) {
|
|
233
|
+
const mcp = /^mcp__(.+?)__(.+)$/.exec(tool || "");
|
|
234
|
+
if (mcp) return `${mcp[1]} - ${mcp[2]} (MCP)`;
|
|
235
|
+
return TOOL_WORDS[tool] || tool || "?";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function el(doc, tag, cls, text) {
|
|
239
|
+
const e = doc.createElement(tag);
|
|
240
|
+
if (cls) e.className = cls;
|
|
241
|
+
if (text != null) e.textContent = text;
|
|
242
|
+
return e;
|
|
243
|
+
}
|
|
244
|
+
function span(doc, text, color, extra) {
|
|
245
|
+
const s = el(doc, "span", extra || null, text);
|
|
246
|
+
if (color) s.style.color = color; // CSSOM, never a style attribute (#1300)
|
|
247
|
+
return s;
|
|
248
|
+
}
|
|
249
|
+
/** A Read result's "12\tcode" lines as the terminal shows them (" 12 code"). */
|
|
250
|
+
const unTab = (text, tool) => (tool === "Read" ? text.replace(/^(\d+)\t/gm, (_, n) => `${n.padStart(4)} `) : text.replace(/\t/g, " "));
|
|
251
|
+
|
|
252
|
+
/** One step's block. `onMore`: the folded step, clicked. */
|
|
253
|
+
export function drawStep(doc, step, pal, glyphs, onMore) {
|
|
254
|
+
const box = el(doc, "div", "vh-step");
|
|
255
|
+
box.dataset.step = step.id;
|
|
256
|
+
const line = (cls) => box.appendChild(el(doc, "div", `vh-line${cls ? ` ${cls}` : ""}`));
|
|
257
|
+
if (step.kind === "user") {
|
|
258
|
+
(step.body || "").split("\n").forEach((text, i) => {
|
|
259
|
+
const l = line("vh-prompt");
|
|
260
|
+
l.style.background = pal.promptBand;
|
|
261
|
+
l.appendChild(span(doc, i === 0 ? `${glyphs.prompt} ` : " ", pal.promptFg));
|
|
262
|
+
l.appendChild(span(doc, text || " ", pal.promptText));
|
|
263
|
+
});
|
|
264
|
+
if (step.cut) line("vh-dim").textContent = ` ${w("cut", step.cut)}`;
|
|
265
|
+
return box;
|
|
266
|
+
}
|
|
267
|
+
if (step.kind === "assistant") {
|
|
268
|
+
(step.body || "").split("\n").forEach((text, i) => {
|
|
269
|
+
const l = line();
|
|
270
|
+
l.appendChild(i === 0 ? span(doc, `${glyphs.bullet} `, pal.replyFg) : span(doc, " "));
|
|
271
|
+
l.appendChild(span(doc, text || " "));
|
|
272
|
+
});
|
|
273
|
+
if (step.cut) line("vh-dim").textContent = ` ${w("cut", step.cut)}`;
|
|
274
|
+
return box;
|
|
275
|
+
}
|
|
276
|
+
if (step.kind === "tool") {
|
|
277
|
+
const head = line();
|
|
278
|
+
const fg = step.status === "failed" ? pal.failedFg : step.status === "running" || step.status === "interrupted" ? pal.runningFg : pal.toolFg;
|
|
279
|
+
head.appendChild(span(doc, `${glyphs.bullet} `, fg));
|
|
280
|
+
head.appendChild(span(doc, toolWord(step.tool), null, "vh-b"));
|
|
281
|
+
head.appendChild(span(doc, step.subject ? `(${step.subject})` : ""));
|
|
282
|
+
}
|
|
283
|
+
// The result: " ⎿ " then its first line; the rest under it — or the edit's diff rows.
|
|
284
|
+
const resultColor = step.kind === "error" || step.status === "failed" ? pal.errorFg : null;
|
|
285
|
+
let firstRow = true;
|
|
286
|
+
const mark = () => { const s = firstRow ? span(doc, ` ${glyphs.result} `, pal.resultFg) : span(doc, " "); firstRow = false; return s; };
|
|
287
|
+
if (step.diff) {
|
|
288
|
+
for (const h of step.diff) {
|
|
289
|
+
for (const [rows, sign, bg, fg] of [[h.del, "-", pal.delBg, pal.delFg], [h.add, "+", pal.addBg, pal.addFg]]) {
|
|
290
|
+
for (const text of rows) {
|
|
291
|
+
const l = line("vh-res");
|
|
292
|
+
l.appendChild(mark());
|
|
293
|
+
const row = span(doc, `${sign} ${text}`, fg, "vh-diff");
|
|
294
|
+
row.style.background = bg;
|
|
295
|
+
l.appendChild(row);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else if (step.body != null && step.body !== "") {
|
|
300
|
+
for (const text of unTab(step.body, step.tool).split("\n")) {
|
|
301
|
+
const l = line("vh-res");
|
|
302
|
+
l.appendChild(mark());
|
|
303
|
+
l.appendChild(span(doc, text || " ", resultColor));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (step.more) {
|
|
307
|
+
const l = line("vh-res");
|
|
308
|
+
l.appendChild(span(doc, " "));
|
|
309
|
+
const b = el(doc, "button", "vh-more", w("more", step.more));
|
|
310
|
+
b.type = "button";
|
|
311
|
+
b.style.color = pal.resultFg;
|
|
312
|
+
b.addEventListener("click", () => onMore && onMore(step, box, b));
|
|
313
|
+
l.appendChild(b);
|
|
314
|
+
} else if (step.cut) line("vh-res vh-dim").textContent = ` ${w("cut", step.cut)}`;
|
|
315
|
+
return box;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The history above one agent's live screen. `fetch(url)` reads under the View's lease; `alive()` is false once the
|
|
320
|
+
* View moved on (nothing is drawn then). `root` is the block panel-view.js puts above the screen it shows.
|
|
321
|
+
*/
|
|
322
|
+
export function createHistory({ name, fetch, alive = () => true, doc = document, font = "monospace", cols = 0 }) {
|
|
323
|
+
ensureStylesheet(doc);
|
|
324
|
+
const root = el(doc, "div", "vh");
|
|
325
|
+
root.setAttribute("data-trust", "panel-content");
|
|
326
|
+
if (cols > 0) root.style.width = `${cols}ch`;
|
|
327
|
+
const pages = el(doc, "div", "vh-pages");
|
|
328
|
+
const capNote = el(doc, "div", "vh-cap");
|
|
329
|
+
const divider = el(doc, "div", "vh-divider", w("divider"));
|
|
330
|
+
divider.setAttribute("role", "separator");
|
|
331
|
+
root.append(pages, divider);
|
|
332
|
+
let pal = DEFAULT_PALETTE, glyphs = null;
|
|
333
|
+
let key = null, before = null, shown = 0, busy = false;
|
|
334
|
+
const base = `/api/pane/${encodeURIComponent(name)}?transcript=1`;
|
|
335
|
+
const read = async (query) => {
|
|
336
|
+
const r = await fetch(`${base}${query}`);
|
|
337
|
+
// An older server answers the pane's text here: not this history.
|
|
338
|
+
const type = r.headers && typeof r.headers.get === "function" ? String(r.headers.get("Content-Type") || "") : "";
|
|
339
|
+
if (!r.ok) return { failed: true, status: r.status };
|
|
340
|
+
if (!type.includes("application/json")) return { unsupported: true };
|
|
341
|
+
const body = await r.json();
|
|
342
|
+
if (!body || body.view_history !== 1) return { unsupported: true };
|
|
343
|
+
return body;
|
|
344
|
+
};
|
|
345
|
+
const block = (steps, end) => {
|
|
346
|
+
const frag = doc.createDocumentFragment();
|
|
347
|
+
for (const s of steps) {
|
|
348
|
+
const b = drawStep(doc, s, pal, glyphs, more);
|
|
349
|
+
b.dataset.end = String(end);
|
|
350
|
+
frag.appendChild(b);
|
|
351
|
+
}
|
|
352
|
+
return frag;
|
|
353
|
+
};
|
|
354
|
+
async function more(step, box, button) {
|
|
355
|
+
if (button.disabled) return;
|
|
356
|
+
button.disabled = true;
|
|
357
|
+
try {
|
|
358
|
+
const body = await read(`&step=${encodeURIComponent(step.id)}&end=${encodeURIComponent(box.dataset.end)}&key=${encodeURIComponent(key || "")}`);
|
|
359
|
+
if (!alive()) return;
|
|
360
|
+
if (!body.supported || !body.step) throw new Error("gone");
|
|
361
|
+
const whole = drawStep(doc, body.step, pal, glyphs, more);
|
|
362
|
+
whole.dataset.end = box.dataset.end;
|
|
363
|
+
box.replaceWith(whole);
|
|
364
|
+
} catch {
|
|
365
|
+
if (!alive()) return;
|
|
366
|
+
button.disabled = false;
|
|
367
|
+
if (!box.querySelector(".vh-more-failed")) button.parentNode.insertBefore(span(doc, ` ${w("moreFailed")}`, pal.errorFg, "vh-more-failed"), button.nextSibling);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
root,
|
|
372
|
+
/** Older steps exist and the cap is not reached. */
|
|
373
|
+
hasOlder: () => before !== null && shown < HISTORY_STEP_CAP,
|
|
374
|
+
/**
|
|
375
|
+
* The newest page, minus what the live `screen` (ANSI) shows. "ok" | "empty" (nothing before the screen) |
|
|
376
|
+
* "unsupported" (no history here) | "failed" (with `status`).
|
|
377
|
+
*/
|
|
378
|
+
async first(screen) {
|
|
379
|
+
pal = samplePalette(screen);
|
|
380
|
+
glyphs = { prompt: drawable(pal.promptGlyph, font, doc), bullet: drawable(pal.bullet, font, doc), result: drawable(pal.resultGlyph, font, doc) };
|
|
381
|
+
let body;
|
|
382
|
+
try { body = await read(""); } catch { return { kind: "failed", status: 0 }; }
|
|
383
|
+
if (body.failed) return { kind: "failed", status: body.status };
|
|
384
|
+
if (body.unsupported) return { kind: "unsupported" };
|
|
385
|
+
// No history here at all (not Claude, or no reader) — or none YET (a session that has written nothing so far):
|
|
386
|
+
// that one is only nothing earlier, asked again next time.
|
|
387
|
+
if (!body.supported) return { kind: body.reason === "unsupported" || body.reason === "unavailable" ? "unsupported" : "empty" };
|
|
388
|
+
key = body.key; before = body.before;
|
|
389
|
+
let steps = beforeScreen(body.steps || [], screen);
|
|
390
|
+
let end = body.end;
|
|
391
|
+
// Everything in the newest page is on the screen: the page before it is the history.
|
|
392
|
+
if (!steps.length && before !== null) {
|
|
393
|
+
let older;
|
|
394
|
+
try { older = await read(`&before=${before}&key=${encodeURIComponent(key)}`); } catch { older = { failed: true, status: 0 }; }
|
|
395
|
+
if (!older.failed && older.supported && !older.reset) { steps = older.steps || []; before = older.before; end = older.end; }
|
|
396
|
+
}
|
|
397
|
+
if (!alive()) return { kind: "failed", status: 0 };
|
|
398
|
+
if (!steps.length) return { kind: "empty" };
|
|
399
|
+
pages.replaceChildren(block(steps, end));
|
|
400
|
+
shown = steps.length;
|
|
401
|
+
return { kind: "ok" };
|
|
402
|
+
},
|
|
403
|
+
/** The page before what is shown, drawn above it. "ok" | "none" | "capped" | "failed". */
|
|
404
|
+
async older() {
|
|
405
|
+
if (busy) return { kind: "none" };
|
|
406
|
+
if (before === null) return { kind: "none" };
|
|
407
|
+
if (shown >= HISTORY_STEP_CAP) {
|
|
408
|
+
if (!capNote.isConnected) { capNote.textContent = w("capped", HISTORY_STEP_CAP); root.prepend(capNote); }
|
|
409
|
+
return { kind: "capped" };
|
|
410
|
+
}
|
|
411
|
+
busy = true;
|
|
412
|
+
try {
|
|
413
|
+
const body = await read(`&before=${before}&key=${encodeURIComponent(key || "")}`);
|
|
414
|
+
if (!alive()) return { kind: "failed" };
|
|
415
|
+
if (body.failed || body.unsupported || !body.supported) return { kind: "failed" };
|
|
416
|
+
// Another transcript now (a new session): what is shown stays; nothing older of it.
|
|
417
|
+
if (body.reset) { before = null; return { kind: "none" }; }
|
|
418
|
+
before = body.before;
|
|
419
|
+
const steps = body.steps || [];
|
|
420
|
+
if (steps.length) { pages.prepend(block(steps, body.end)); shown += steps.length; }
|
|
421
|
+
return { kind: "ok" };
|
|
422
|
+
} catch { return { kind: "failed" }; }
|
|
423
|
+
finally { busy = false; }
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** This module's own stylesheet, linked once (a tab opened before an upgrade has an app.css without these rules). */
|
|
429
|
+
function ensureStylesheet(doc) {
|
|
430
|
+
try {
|
|
431
|
+
if (!doc.head || doc.querySelector('link[data-vh="1"]')) return;
|
|
432
|
+
const link = doc.createElement("link");
|
|
433
|
+
link.setAttribute("rel", "stylesheet");
|
|
434
|
+
link.setAttribute("href", "/assets/view-history.css");
|
|
435
|
+
link.setAttribute("data-vh", "1");
|
|
436
|
+
doc.head.appendChild(link);
|
|
437
|
+
} catch { /* the history still reads without it */ }
|
|
438
|
+
}
|
|
@@ -154,6 +154,14 @@ register("view", {
|
|
|
154
154
|
"usage.stale.transient": "not updated · cached at {0}",
|
|
155
155
|
"usage.stale.error": "couldn't read · cached at {0}",
|
|
156
156
|
"usage.metric.reset_since": "reset since {0}",
|
|
157
|
+
"usage.capacity.line": "100% ≈ {0} ({1}–{2}) · ≈ {3} tokens · from {4} weeks",
|
|
158
|
+
"usage.capacity.line_tokens": "100% ≈ {0} tokens ({1}–{2}) · from {3} weeks",
|
|
159
|
+
"usage.capacity.not_enough": "Not enough usage yet to measure 100% (Δ {0}% of 10%)",
|
|
160
|
+
"usage.capacity.only_agend": "Counts only this fleet's agents; use of the same account elsewhere makes this low.",
|
|
161
|
+
"usage.capacity.mixed": "Bases disagree — possibly used outside this fleet",
|
|
162
|
+
"usage.capacity.best_fit_api": "API-equivalent fits best: ±{0}% across {1} weeks",
|
|
163
|
+
"usage.capacity.best_fit_output": "Output tokens fit best: ±{0}% across {1} weeks",
|
|
164
|
+
"usage.capacity.best_fit_all": "All tokens fit best: ±{0}% across {1} weeks",
|
|
157
165
|
"usage.error.http": "Usage request failed (HTTP {0}).",
|
|
158
166
|
"usage.error.billing_http": "Billing request failed (HTTP {0}).",
|
|
159
167
|
"usage.error.invalid_response": "Invalid response from usage endpoint.",
|
|
@@ -414,6 +422,14 @@ register("view", {
|
|
|
414
422
|
"usage.stale.transient": "暫時無法更新 · 快取於 {0}",
|
|
415
423
|
"usage.stale.error": "讀取失敗 · 快取於 {0}",
|
|
416
424
|
"usage.metric.reset_since": "已於 {0} 重置",
|
|
425
|
+
"usage.capacity.line": "100% ≈ {0}({1}–{2})· 約 {3} token · 依 {4} 週",
|
|
426
|
+
"usage.capacity.line_tokens": "100% ≈ {0} token({1}–{2})· 依 {3} 週",
|
|
427
|
+
"usage.capacity.not_enough": "用量還不夠估算 100%(已用 Δ {0}%,需 10%)",
|
|
428
|
+
"usage.capacity.only_agend": "只計算這個 fleet 的 agent;同一帳號在別處的使用會讓這個值偏低。",
|
|
429
|
+
"usage.capacity.mixed": "各換算方式不一致——可能有 fleet 以外的使用",
|
|
430
|
+
"usage.capacity.best_fit_api": "「API 等值」最吻合:{1} 週內 ±{0}%",
|
|
431
|
+
"usage.capacity.best_fit_output": "「輸出 token」最吻合:{1} 週內 ±{0}%",
|
|
432
|
+
"usage.capacity.best_fit_all": "「全部 token」最吻合:{1} 週內 ±{0}%",
|
|
417
433
|
"usage.error.http": "用量請求失敗(HTTP {0})。",
|
|
418
434
|
"usage.error.billing_http": "計費請求失敗(HTTP {0})。",
|
|
419
435
|
"usage.error.invalid_response": "用量端點回傳了無效資料。",
|