@songsid/agend 2.3.4-beta.1 → 2.3.4-beta.10
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/agend-home.d.ts +13 -0
- package/dist/agend-home.js +42 -0
- package/dist/agend-home.js.map +1 -1
- 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 +10 -2
- 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/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +89 -4
- package/dist/backend/claude-code.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-session-lookup.d.ts +35 -0
- package/dist/backend/codex-session-lookup.js +93 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +27 -0
- package/dist/backend/codex.js +124 -23
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +13 -6
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +68 -5
- 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/backend/types.d.ts +6 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +6 -0
- package/dist/backend-availability.js +13 -5
- package/dist/backend-availability.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- 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 +27 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +238 -18
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/inbox-temp-name.d.ts +9 -0
- package/dist/channel/inbox-temp-name.js +13 -0
- package/dist/channel/inbox-temp-name.js.map +1 -0
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/chat-log-ids.d.ts +48 -0
- package/dist/chat-log-ids.js +182 -0
- package/dist/chat-log-ids.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +8 -1
- package/dist/classic-channel-manager.js +24 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +4 -6
- package/dist/cli.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 +52 -0
- package/dist/daemon.js +530 -34
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/dialog-screen-text.d.ts +16 -0
- package/dist/dialog-screen-text.js +51 -0
- package/dist/dialog-screen-text.js.map +1 -0
- 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 +147 -11
- package/dist/fleet-manager.js +747 -118
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-patch.d.ts +3 -0
- package/dist/fleet-yaml-patch.js +103 -0
- package/dist/fleet-yaml-patch.js.map +1 -0
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +10 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/install-stream.d.ts +53 -0
- package/dist/install-stream.js +134 -0
- package/dist/install-stream.js.map +1 -0
- package/dist/instance-lifecycle.d.ts +20 -2
- package/dist/instance-lifecycle.js +80 -12
- 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/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-resume-summary.d.ts +37 -0
- package/dist/kiro-resume-summary.js +85 -0
- package/dist/kiro-resume-summary.js.map +1 -0
- package/dist/locale.js +82 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +30 -0
- package/dist/log-service.js +36 -3
- 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/login-controller.js +9 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.d.ts +6 -0
- package/dist/login-manager.js +10 -1
- package/dist/login-manager.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 +31 -8
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +22 -2
- package/dist/needs-you.js +17 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/office-share-page.d.ts +1 -1
- package/dist/office-share-page.js +6 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/outbound-handlers.js +59 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +9 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/reply-buttons-controller.js +10 -0
- package/dist/reply-buttons-controller.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/selection-card.d.ts +2 -3
- package/dist/selection-card.js +99 -26
- package/dist/selection-card.js.map +1 -1
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +12 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/stall-reporter.d.ts +21 -0
- package/dist/stall-reporter.js +82 -0
- package/dist/stall-reporter.js.map +1 -0
- package/dist/tmux-control.d.ts +2 -2
- package/dist/tmux-control.js +4 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +6 -3
- package/dist/tmux-exec-helper.js +115 -53
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +6 -0
- package/dist/tmux-manager.js +17 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.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/tunnel/cloudflared-install.d.ts +4 -0
- package/dist/tunnel/cloudflared-install.js +20 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +8 -1
- package/dist/tunnel/cloudflared.js +34 -6
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.d.ts +3 -3
- package/dist/tunnel/lease.js +23 -19
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js +6 -7
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/ui/chat-store.js +20 -3
- package/dist/ui/chat-strings.js +4 -4
- package/dist/ui/chat-thread.js +52 -10
- package/dist/ui/fleet-canvas.js +4 -3
- package/dist/ui/fleet-office-model.js +55 -22
- package/dist/ui/fleet-office-overview.js +6 -38
- package/dist/ui/fleet-office-pack.js +4 -3
- package/dist/ui/fleet-office-skin.js +114 -21
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +41 -9
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +2 -0
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/panel-needs.js +17 -8
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app-i18n.js +4 -4
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-shell.js +4 -2
- package/dist/ui/shared/app-stream.js +26 -0
- package/dist/ui/shared/app.css +51 -3
- package/dist/ui/shared/office/guild.png +0 -0
- package/dist/ui/shared/office/school.png +0 -0
- package/dist/ui/shared/panel-view.js +339 -17
- 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 +42 -0
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +79 -0
- package/dist/unattended-install.js.map +1 -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 +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +31 -0
- package/dist/view-api.js +186 -41
- 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 +15 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +20 -0
- package/dist/web-chat-history.js +21 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +5 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-login.d.ts +4 -3
- package/dist/web-login.js +23 -14
- package/dist/web-login.js.map +1 -1
- package/dist/web-office-pack-tools.d.ts +1 -1
- package/dist/web-office-pack-tools.js +1 -1
- package/dist/web-office-pack-tools.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/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +20 -8
- package/dist/web-session.js.map +1 -1
- package/launcher/agend-install.cjs +16 -2
- package/launcher/private-home.cjs +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
// - every reconnect catches up once the new stream speaks (catchUp(), with the page's cursor — #1566/#1578), so what
|
|
28
28
|
// was said meanwhile is shown, once.
|
|
29
29
|
// - a 401 is the session ending: nothing is retried (agend-auth.js says so).
|
|
30
|
+
// - M-2260: a catch-up after a long absence carries only the newest messages (the server's budget) and says so
|
|
31
|
+
// (`truncated`): "gap" tells the chats to read their newest history again — it converges like a reload, never by
|
|
32
|
+
// asking a slow link for hours of replies in one answer.
|
|
30
33
|
// - suspend()/resume(): the page is leaving (pagehide) / came back from the back-forward cache.
|
|
31
34
|
|
|
32
35
|
// #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
|
|
@@ -79,6 +82,22 @@ export function createStream(opts) {
|
|
|
79
82
|
try { return Object.freeze({ source: "sse", receivedAt: now() }); } catch { return undefined; }
|
|
80
83
|
}
|
|
81
84
|
function setConnection(c) { if (state.connection !== c) { state.connection = c; emit("connection", c); } }
|
|
85
|
+
|
|
86
|
+
// M-2260: a tab the browser froze for hours gets every heartbeat it missed at once when it wakes — thousands of status
|
|
87
|
+
// frames, each a whole snapshot, each redrawing the page (seconds of work before the newest message shows). A status
|
|
88
|
+
// that comes after a quiet moment is delivered at once, as before; more statuses before the next turn of the page's
|
|
89
|
+
// task loop replace the waiting one, delivered then — or before any other event, so the order of events never changes.
|
|
90
|
+
// The next turn is a 0 ms timer: in a hidden tab the browser may run it late (a minute when throttled) — a status
|
|
91
|
+
// then waits that long while nobody looks (Needs you, the title's count and notifications ride `needs`, never held).
|
|
92
|
+
let pendingStatus = null, statusBurst = false;
|
|
93
|
+
const turn = () => env.setTimeout(endBurst, 0);
|
|
94
|
+
function endBurst() { statusBurst = false; flushStatus(); }
|
|
95
|
+
function flushStatus() { const d = pendingStatus; pendingStatus = null; if (d !== null) live("status", d, null); }
|
|
96
|
+
function liveStatus(data) {
|
|
97
|
+
if (statusBurst) { pendingStatus = data; return; }
|
|
98
|
+
statusBurst = true; turn();
|
|
99
|
+
live("status", data, null);
|
|
100
|
+
}
|
|
82
101
|
function setHydration(h) { if (state.hydration !== h) { state.hydration = h; emit("hydration", h); } }
|
|
83
102
|
|
|
84
103
|
/** A live event: delivered now, or held (in order) while a catch-up snapshot is on its way. */
|
|
@@ -115,6 +134,7 @@ export function createStream(opts) {
|
|
|
115
134
|
if (polling) { if (!opts.join) pollAgain = true; return polling; }
|
|
116
135
|
const mine = ++pollSeq, ep = readEpoch;
|
|
117
136
|
let status = 0; // this read's HTTP status — its own, never another read's
|
|
137
|
+
flushStatus(); // a status that came before this read is older than its snapshot
|
|
118
138
|
if (sse && !held) held = [];
|
|
119
139
|
let limit = null;
|
|
120
140
|
const read = (async () => {
|
|
@@ -128,6 +148,9 @@ export function createStream(opts) {
|
|
|
128
148
|
if (d.status) emit("status", d.status);
|
|
129
149
|
for (const m of d.messages || []) emit("message", m, { source: "poll" });
|
|
130
150
|
if (typeof d.cursor === "string") state.cursor = d.cursor;
|
|
151
|
+
// M-2260: the server sent only the newest messages of a long absence (its catch-up budget): the chats read their
|
|
152
|
+
// newest history again, as a reload would, instead of this read carrying everything that was missed.
|
|
153
|
+
if (d.truncated === true) emit("gap", {});
|
|
131
154
|
if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
|
|
132
155
|
if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
|
|
133
156
|
if (Array.isArray(d.reply_buttons)) for (const u of d.reply_buttons) emit("reply_buttons", u); // #1266
|
|
@@ -157,6 +180,7 @@ export function createStream(opts) {
|
|
|
157
180
|
}
|
|
158
181
|
/** #1580 review: the page lets go of everything in flight — reads, the queued poll, the catch-up and its retry wait. */
|
|
159
182
|
function cancelWork() {
|
|
183
|
+
pendingStatus = null; // a page let go of (suspend, end, close) draws no waiting status
|
|
160
184
|
readEpoch++; epoch++;
|
|
161
185
|
pollSeq++; polling = null; pollAgain = false; held = null;
|
|
162
186
|
env.clearTimeout(retryTimer); retryTimer = null;
|
|
@@ -225,6 +249,8 @@ export function createStream(opts) {
|
|
|
225
249
|
else alive();
|
|
226
250
|
// #1823: a clock reading is about now, not about order: it is never held behind a catch-up (it would be stale).
|
|
227
251
|
if (name === "clock") { emit(name, data); return; }
|
|
252
|
+
if (name === "status") { liveStatus(data); return; }
|
|
253
|
+
flushStatus(); // a status waiting goes first: the order never changes
|
|
228
254
|
live(name, data, name === "message" ? e.lastEventId : null, ingress);
|
|
229
255
|
});
|
|
230
256
|
}
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -623,9 +623,21 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
623
623
|
Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
|
|
624
624
|
the meta, body and tools into one flex row. */
|
|
625
625
|
.msg.status-line { align-self: stretch; font-size: var(--fs-sm); color: var(--text-3); }
|
|
626
|
-
|
|
627
|
-
.msg.status-line
|
|
626
|
+
/* Each line is a row (.st-ln); the line breaks between rows are text for copying, not extra rows. */
|
|
627
|
+
.msg.status-line .st-text { border-left: 2px solid var(--border); padding-left: var(--s-3); }
|
|
628
|
+
.msg.status-line .st-ln { display: block; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
629
|
+
.msg.status-line .st-ln:empty::after { content: " "; }
|
|
630
|
+
/* Folded (the user, hmsg-d6ac6dfb): two lines — the header and the first line — each one row with an ellipsis; the
|
|
631
|
+
button sits at the end of line 2. The text opens it too. */
|
|
632
|
+
.msg.status-line.folded .body { display: flex; align-items: flex-end; gap: var(--s-2); }
|
|
633
|
+
.msg.status-line.folded .st-text { flex: 1 1 auto; min-width: 0; }
|
|
634
|
+
.msg.status-line.folded.overflows .st-text { cursor: pointer; }
|
|
635
|
+
/* Its control only while something is hidden (two short rows that fit: plain, no control). */
|
|
636
|
+
.msg.status-line.folded:not(.overflows) .st-more { display: none; }
|
|
637
|
+
.msg.status-line.folded .st-ln { white-space: pre; overflow: hidden; text-overflow: ellipsis; }
|
|
638
|
+
.msg.status-line.folded .st-ln:nth-child(n+3) { display: none; }
|
|
628
639
|
.msg.status-line .st-more { border: 0; background: none; color: var(--text-2); cursor: pointer; font-size: var(--fs-xs); padding: var(--s-1) 0 0 var(--s-3); }
|
|
640
|
+
.msg.status-line.folded .st-more { flex: none; padding: 0 var(--s-2) 0 0; white-space: nowrap; }
|
|
629
641
|
.msg.status-line .st-more:hover { color: var(--text); }
|
|
630
642
|
.msg-tools { display: flex; gap: var(--s-1); margin-top: var(--s-1); opacity: 0; transition: opacity var(--t-fast); }
|
|
631
643
|
/* #2147: bounded stock toolbar paint, including fixed descendants. Padding preserves focus outlines. */
|
|
@@ -1323,6 +1335,21 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1323
1335
|
/* #1844 Fleet overview → Docs: the arrow says "opens a new tab". */
|
|
1324
1336
|
.hd-docs .hd-ext { color: var(--text-3); }
|
|
1325
1337
|
.v-term { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; background: #000000; position: relative; }
|
|
1338
|
+
/* M-2262: the terminal's box — the scrolling terminal, and its scrollback controls over it (they never scroll with it). */
|
|
1339
|
+
/* --v-term-rev: the View's mark (panel-view.js VIEW_REV): a tab still on an older stylesheet reads this one again. */
|
|
1340
|
+
.v-term-box { --v-term-rev: 2; flex: 1; min-height: 0; display: flex; flex-direction: column; position: relative; }
|
|
1341
|
+
.v-term:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
1342
|
+
.v-term-earlier, .v-term-latest { position: absolute; right: var(--s-4); z-index: 2; display: inline-flex; align-items: center; gap: 4px; opacity: .88; }
|
|
1343
|
+
.v-term-earlier { top: var(--s-2); }
|
|
1344
|
+
.v-term-latest { bottom: var(--s-3); opacity: 1; }
|
|
1345
|
+
.v-term-logbar { flex-shrink: 0; margin: 0; padding: var(--s-2) var(--s-4); background: #111111; border-bottom: 1px solid #2a2a2a; text-align: right; font-size: var(--fs-sm); }
|
|
1346
|
+
.v-term-logbar a { display: inline-flex; align-items: center; gap: 4px; color: #9cc8ff; text-decoration: underline; text-underline-offset: 2px; min-height: 28px; }
|
|
1347
|
+
@media (pointer: coarse) { .v-term-logbar a { min-height: 44px; } }
|
|
1348
|
+
.v-term-latest.has-new { background: var(--accent-fill); border-color: transparent; color: var(--on-accent); }
|
|
1349
|
+
.v-term-earlier:hover, .v-term-earlier:focus-visible { opacity: 1; }
|
|
1350
|
+
.v-term-history-note { position: absolute; left: var(--s-3); right: var(--s-3); bottom: var(--s-3); z-index: 2; margin: 0; padding: var(--s-1) var(--s-3); border-radius: var(--r-sm); background: rgba(20, 20, 20, .92); border: 1px solid #3a3a3a; color: #e0e0e0; font-size: var(--fs-sm); }
|
|
1351
|
+
@media (max-width: 600px) { .v-term-history-note { left: var(--s-4); right: var(--s-4); } }
|
|
1352
|
+
@media (pointer: coarse) { .v-term-earlier, .v-term-latest { min-height: 44px; padding-left: var(--s-3); padding-right: var(--s-3); } }
|
|
1326
1353
|
.v-pre { margin: 0; min-width: 100%; min-height: 100%; font-family: var(--font-mono); line-height: 1.2; white-space: pre; color: #d0d0d0; }
|
|
1327
1354
|
.v-pre .ansi[data-b] { font-weight: var(--fw-bold); }
|
|
1328
1355
|
/* #1582: faint text (SGR 2) — a CLI's suggestion or placeholder, never input — stays faint here. */
|
|
@@ -1365,6 +1392,11 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1365
1392
|
.u-stale { font-weight: var(--fw-semibold); }
|
|
1366
1393
|
.u-stale-detail { color: var(--text-3); font-size: var(--fs-xs); }
|
|
1367
1394
|
.u-sub { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
|
|
1395
|
+
/* #3066 §1: a weekly quota's measured 100% under its bar, and its notes (the fleet-only limit always). */
|
|
1396
|
+
.u-cap { margin-top: 4px; font-size: var(--fs-xs); }
|
|
1397
|
+
.u-cap-line { color: var(--text-2); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
1398
|
+
.u-cap-wait .u-cap-line { color: var(--text-3); }
|
|
1399
|
+
.u-cap-note { color: var(--text-3); overflow-wrap: anywhere; }
|
|
1368
1400
|
.u-err { color: var(--warn); font-size: var(--fs-sm); }
|
|
1369
1401
|
.u-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
|
|
1370
1402
|
/* #1673: the usage rail beside the page on a wide screen (header-tools.js UsageRail; shown only at min-width 1200px). */
|
|
@@ -1860,6 +1892,12 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1860
1892
|
.oo-canvas:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
|
1861
1893
|
.oo-canvas.hot { cursor: pointer; }
|
|
1862
1894
|
/* The canvas controls, as the org chart canvas's (.oc-zoom, .oc-mini): over the stage's corners. */
|
|
1895
|
+
/* #2946: the key to the items carried between desks — a strip under the bar, as the org chart's (.oc-legend). */
|
|
1896
|
+
.oo-key { font-size: var(--fs-xs); color: var(--text-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 4px 10px; }
|
|
1897
|
+
.oo-key-rows { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); margin: 0; padding: 0; list-style: none; }
|
|
1898
|
+
.oo-key-row { display: inline-flex; align-items: center; gap: 6px; }
|
|
1899
|
+
.oo-key-icon { width: 32px; height: 32px; flex: none; image-rendering: pixelated; }
|
|
1900
|
+
.oo-key-btn.on { border-color: var(--accent); }
|
|
1863
1901
|
.oo-zoom { z-index: 6; }
|
|
1864
1902
|
.oo-zoom .icon-btn { color: var(--text); }
|
|
1865
1903
|
/* The overview's box (left, top, width, height) is set by the page, inside the stage and scaled to it (miniBox). */
|
|
@@ -2078,6 +2116,16 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
2078
2116
|
.us-meta { display: block; font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
|
|
2079
2117
|
.us-est { display: block; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--text-3); }
|
|
2080
2118
|
.us-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
|
2119
|
+
/* #3066 §3: a row's numbers carry small marks (◐ partly priced, † the CLI's amount, ≈ estimated) and "—" with a reason,
|
|
2120
|
+
each said in its tooltip; the key under the table names only the marks that appear. */
|
|
2121
|
+
.us-mark { margin-left: 2px; color: var(--text-3); font-size: var(--fs-xs); cursor: help; }
|
|
2122
|
+
.us-mark.us-approx { margin-left: 0; margin-right: 1px; }
|
|
2123
|
+
.us-none { cursor: help; }
|
|
2124
|
+
.us-key { margin-top: 6px; }
|
|
2125
|
+
/* #3066 §2: each model's speed after the first token. */
|
|
2126
|
+
.us-speed { margin-top: 12px; }
|
|
2127
|
+
.us-speed .row-item { gap: 8px; flex-wrap: wrap; }
|
|
2128
|
+
.us-speed .num { font-variant-numeric: tabular-nums; }
|
|
2081
2129
|
.us-trend, .us-heat { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
|
|
2082
2130
|
.us-trend h3, .us-heat h3, .us-rest h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
|
|
2083
2131
|
.us-scroll { overflow-x: auto; max-width: 100%; }
|
|
@@ -2286,7 +2334,7 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
2286
2334
|
.p-fleet > .seg, .p-settings > .seg,
|
|
2287
2335
|
.p-fleet > .panel-body, .p-needs > .panel-body, .p-notices > .panel-body, .p-settings > .panel-body,
|
|
2288
2336
|
.p-log > .panel-body, .p-details > .panel-body, .p-view > .panel-body,
|
|
2289
|
-
.p-log > .log-bar, .p-view > .v-term, .p-view > .v-id, .p-view > .v-watching { position: relative; z-index: 1; }
|
|
2337
|
+
.p-log > .log-bar, .p-view > .v-term-box, .p-view > .v-id, .p-view > .v-watching { position: relative; z-index: 1; }
|
|
2290
2338
|
/* Keep the existing Apply bar's sticky behavior, in its own bounded stock band. */
|
|
2291
2339
|
.p-settings > .s-pending { z-index: 1; }
|
|
2292
2340
|
|
|
Binary file
|
|
Binary file
|
|
@@ -26,7 +26,7 @@ import { viewStore } from "./view-roster-store.js";
|
|
|
26
26
|
/** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
|
|
27
27
|
const fleetUnreachable = () => appStore.get().connection === "reconnecting";
|
|
28
28
|
import { navigate } from "./app-nav.js";
|
|
29
|
-
import { viewPath } from "./app-route.js";
|
|
29
|
+
import { logPath, sectionPath, viewPath } from "./app-route.js";
|
|
30
30
|
import { Dialog } from "./ui-dialog.js";
|
|
31
31
|
import { Empty, ErrorState, Skeleton } from "./ui-states.js";
|
|
32
32
|
import { Icon } from "./ui-icons.js";
|
|
@@ -41,6 +41,44 @@ function tn(key, values = {}) {
|
|
|
41
41
|
for (const [k, v] of Object.entries(values)) s = s.split(`{${k}}`).join(String(v));
|
|
42
42
|
return s;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* v2.3.4-beta.4 P1 (hmsg-985db235, hmsg-5ab1997d): this module is loaded on first use, so a tab opened before an upgrade
|
|
46
|
+
* runs the new View with the stylesheet and the View's words it loaded at its start. Without the View's layout rules
|
|
47
|
+
* (.v-term-box) the terminal box grew with its text: the text shrank every second (fit() measured the box its own text
|
|
48
|
+
* sized), a normal-buffer CLI's View showed its whole history from the top, and new words showed as raw keys. Each is
|
|
49
|
+
* found by a mark the current files carry — VIEW_REV in app.css (the --v-term-rev property) and view-strings.js (the
|
|
50
|
+
* view.rev word) — and a stale one is read again, once per page: the stylesheet re-linked after the one in force, the
|
|
51
|
+
* words imported again (register() replaces them). The query only makes it a new request: /assets/ serves the file.
|
|
52
|
+
*/
|
|
53
|
+
export const VIEW_REV = "2";
|
|
54
|
+
const healed = { css: false, words: false };
|
|
55
|
+
export function healStalePage(doc = typeof document !== "undefined" ? document : null, load = (u) => import(u), onWords = () => {}) {
|
|
56
|
+
if (!doc) return { css: false, words: false };
|
|
57
|
+
const out = { css: false, words: false };
|
|
58
|
+
try {
|
|
59
|
+
const box = doc.querySelector && doc.querySelector(".v-term-box");
|
|
60
|
+
// Only a page that has the stylesheet linked (the app; never a test's page without one) and an older one in force.
|
|
61
|
+
const linked = [...(doc.querySelectorAll ? doc.querySelectorAll('link[rel="stylesheet"]') : [])]
|
|
62
|
+
.some((l) => String(l.getAttribute("href") || "").split("?")[0] === "/assets/app.css");
|
|
63
|
+
if (box && linked && !healed.css) {
|
|
64
|
+
const cs = typeof getComputedStyle === "function" ? getComputedStyle(box) : null;
|
|
65
|
+
const rev = cs && typeof cs.getPropertyValue === "function" ? String(cs.getPropertyValue("--v-term-rev")).trim().replace(/^"|"$/g, "") : VIEW_REV;
|
|
66
|
+
if (rev !== VIEW_REV) {
|
|
67
|
+
healed.css = true; out.css = true;
|
|
68
|
+
const link = doc.createElement("link");
|
|
69
|
+
link.setAttribute("rel", "stylesheet"); link.setAttribute("href", `/assets/app.css?rev=${VIEW_REV}-${Date.now().toString(36)}`);
|
|
70
|
+
doc.head.appendChild(link);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch { /* never in the View's way */ }
|
|
74
|
+
if (t("view.rev") !== VIEW_REV && !healed.words) {
|
|
75
|
+
healed.words = true; out.words = true;
|
|
76
|
+
Promise.resolve(load(`./view-strings.js?rev=${VIEW_REV}-${Date.now().toString(36)}`)).then(() => onWords(), () => {});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** For tests: as on a fresh page. */
|
|
81
|
+
export function resetHealed() { healed.css = false; healed.words = false; }
|
|
44
82
|
const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
45
83
|
const store = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this page only */ } };
|
|
46
84
|
|
|
@@ -198,8 +236,8 @@ export function ViewPanel({ route, navKey }) {
|
|
|
198
236
|
else if (!it) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
|
|
199
237
|
else if (guest && !trusted) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="view" title=${tn("guestNeedsTrusted")} hint=${tn("guestNeedsTrustedHint")} /></div>`;
|
|
200
238
|
else if (guest) body = html`<p data-trust="panel-content" class="v-watching" role="note">${tn("guestWatching")}</p>
|
|
201
|
-
<${Terminal} key=${name} name=${name} lease=${lease} density=${density} every=${GUEST_PANE_MS} />`;
|
|
202
|
-
else body = html`<${Terminal} key=${name} name=${name} lease=${lease} density=${density} />
|
|
239
|
+
<${Terminal} key=${name} name=${name} lease=${lease} density=${density} every=${GUEST_PANE_MS} backend=${it.backend || ""} signedIn=${false} />`;
|
|
240
|
+
else body = html`<${Terminal} key=${name} name=${name} lease=${lease} density=${density} backend=${it.backend || ""} signedIn=${!viewOnly} />
|
|
203
241
|
<${ViewIdentity} it=${it} viewOnly=${viewOnly} onEdit=${() => setDialog("edit")} />`;
|
|
204
242
|
|
|
205
243
|
return html`<div data-effect-panel="view" class="panel p-view">
|
|
@@ -214,11 +252,84 @@ export function ViewPanel({ route, navKey }) {
|
|
|
214
252
|
|
|
215
253
|
/** #2855: how often a guest's View reads the pane (the server allows it two reads a second). */
|
|
216
254
|
const GUEST_PANE_MS = 1000;
|
|
217
|
-
/**
|
|
218
|
-
|
|
255
|
+
/**
|
|
256
|
+
* The live terminal: the pane every 800 ms (passive), drawn as escaped markup, its font fitted to the pane's grid.
|
|
257
|
+
*
|
|
258
|
+
* M-2262 (the user, hmsg-435f6df6): scrolling back. Following (the default), it shows the pane's screen as it is now.
|
|
259
|
+
* Scrolling up — the wheel, a touch drag, PageUp/↑/Home, or "Earlier output" — reads the pane's scrollback once
|
|
260
|
+
* (`?history=1`: tmux's history above the screen, bounded) and shows it: reading. While reading, nothing moves the
|
|
261
|
+
* person: the refresh keeps reading the screen only to say "New output below", and never redraws. "Jump to latest"
|
|
262
|
+
* (or End), or scrolling to the bottom, follows again. Everything happens in this page, over text tmux was asked to
|
|
263
|
+
* capture: never copy-mode and never a key to the pane (hmsg-b8a170c0 — scrolling must never affect delivery).
|
|
264
|
+
*/
|
|
265
|
+
/** How long a scrollback note stays (it covers the terminal's bottom rows). */
|
|
266
|
+
const NOTE_MS = 6000;
|
|
267
|
+
/** A scroll this soon after the person's own wheel, touch, key or pointer is theirs (not one this page made). */
|
|
268
|
+
const GESTURE_MS = 1000;
|
|
269
|
+
/**
|
|
270
|
+
* #2969 review (Prism P3): is `live` (a screen read) still what `shown` ends with — the screen as it was when reading
|
|
271
|
+
* began (the scrollback's last lines, or the screen itself)? Trailing blank rows are not content.
|
|
272
|
+
*/
|
|
273
|
+
export function sameScreen(shown, live) {
|
|
274
|
+
if (shown == null || live == null) return shown === live;
|
|
275
|
+
const trim = (t) => String(t).replace(/\n+$/, "");
|
|
276
|
+
const s = trim(shown).split("\n"), l = trim(live).split("\n");
|
|
277
|
+
return s.length >= l.length && s.slice(s.length - l.length).join("\n") === l.join("\n");
|
|
278
|
+
}
|
|
279
|
+
/** M-2262 survey: CLIs that redraw their whole window (no tmux scrollback even outside the alternate screen). */
|
|
280
|
+
const REDRAWS_IN_PLACE = new Set(["grok"]);
|
|
281
|
+
/** M-2262 survey: the full-screen CLIs (the alternate screen, or redrawn in place) — tmux keeps nothing above their screen. */
|
|
282
|
+
const FULL_SCREEN = new Set(["claude-code", "opencode", "grok"]); // agy: its chat UI unverified — found by the read
|
|
283
|
+
/** The full-screen CLIs whose earlier output the Log tab shows (it reads their transcript: log-service.ts). */
|
|
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;
|
|
292
|
+
/**
|
|
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
|
|
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
|
|
295
|
+
* guest or a View-only reader: they get the plain words), and only for a CLI the Log tab reads.
|
|
296
|
+
*/
|
|
297
|
+
function logLinkFor(name, backend, signedIn) {
|
|
298
|
+
if (!signedIn || !LOG_HAS_HISTORY.has(backend)) return null;
|
|
299
|
+
const on = typeof document !== "undefined" && document.body?.dataset?.logTab === "1";
|
|
300
|
+
return on ? { href: logPath(name), text: tn("earlierInLog") } : { href: sectionPath("web-dashboard"), text: tn("logTabOff") };
|
|
301
|
+
}
|
|
302
|
+
function Terminal({ name, lease, density, every = 800, backend = "", signedIn = false }) {
|
|
219
303
|
const term = useRef(null), pre = useRef(null);
|
|
220
|
-
const grid = useRef({ cols: 0, rows: 0, applied: 0, key: "" });
|
|
304
|
+
const grid = useRef({ cols: 0, rows: 0, applied: 0, key: "", follows: false });
|
|
221
305
|
const [problem, setProblem] = useState(null);
|
|
306
|
+
const [, setWordsRev] = useState(0);
|
|
307
|
+
// "follow" | "loading" | "reading"; the screen's text when reading began, the newest read since; a note about history.
|
|
308
|
+
const [mode, setModeState] = useState("follow");
|
|
309
|
+
const modeRef = useRef("follow");
|
|
310
|
+
const setMode = (m) => { modeRef.current = m; setModeState(m); };
|
|
311
|
+
const [newBelow, setNewBelow] = useState(false);
|
|
312
|
+
const [historyNote, setHistoryNoteState] = useState(null);
|
|
313
|
+
// A CLI found to keep no scrollback (full screen): nothing is offered or read again for it; the note says why, briefly.
|
|
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);
|
|
319
|
+
const logLink = logLinkFor(name, backend, signedIn);
|
|
320
|
+
const noteTimer = useRef(null);
|
|
321
|
+
const setHistoryNote = (note, brief = false) => {
|
|
322
|
+
lease.clear(noteTimer.current); noteTimer.current = null;
|
|
323
|
+
setHistoryNoteState(note);
|
|
324
|
+
if (note && brief) noteTimer.current = lease.timeout(() => setHistoryNoteState(null), NOTE_MS);
|
|
325
|
+
};
|
|
326
|
+
// `reading`: the text shown while reading (the scrollback, or the screen as it was when the person left the bottom);
|
|
327
|
+
// `latest`: the newest screen read; `history`: whether what is shown has the scrollback in it.
|
|
328
|
+
// `drawn` (#2969 review P3): the text actually on the page now — what reading starts from, never a newer read not drawn.
|
|
329
|
+
const screenAt = useRef({ reading: null, latest: null, history: false, drawn: null });
|
|
330
|
+
// #2969 review (Prism P2): when the person last moved the terminal themselves (wheel, touch, key, pointer).
|
|
331
|
+
const gestureAt = useRef(-Infinity);
|
|
332
|
+
const recentGesture = () => performance.now() - gestureAt.current < GESTURE_MS;
|
|
222
333
|
// #1674: the text size as of this render. The pane refresh and the resize handlers below are set up once per mount
|
|
223
334
|
// and keep the fit() of that render; reading the size here, not from that render's props, keeps them on the current
|
|
224
335
|
// choice (they used to put the mount-time size back within one 800 ms refresh).
|
|
@@ -236,16 +347,72 @@ function Terminal({ name, lease, density, every = 800 }) {
|
|
|
236
347
|
const pcs = getComputedStyle(p);
|
|
237
348
|
const lh = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
|
|
238
349
|
const lineH = Number.isFinite(lh) && lh > 0 ? lh : 1.2;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
350
|
+
const byW = innerW / (g.cols * glyphRatio(p)), byH = innerH / (g.rows * lineH);
|
|
351
|
+
const sizeFor = (limit) => (density === "fit" ? Math.max(FIT_FLOOR_PX, Math.min(limit * 0.995, MAX_PX))
|
|
352
|
+
: Math.max(8, Math.min(Math.max(MIN_PX, Math.min(limit * 0.995, MAX_PX)) * (VIEW_SCALE[density] || 1), 32)));
|
|
353
|
+
const size = sizeFor(g.follows ? byW : Math.min(byW, byH));
|
|
242
354
|
g.key = key;
|
|
243
355
|
// A small change is skipped only when growing (the size applied still fits); a smaller box always shrinks it.
|
|
244
356
|
if (size >= g.applied && size - g.applied < 0.75) return;
|
|
245
|
-
|
|
246
|
-
|
|
357
|
+
const padH = (parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom)) || 0;
|
|
358
|
+
const was = g.applied || parseFloat(pcs.fontSize) || 0, before = box.clientHeight - padH;
|
|
359
|
+
const apply = (px) => { g.applied = px; p.style.fontSize = `${px.toFixed(2)}px`; }; // CSSOM, never a style attribute (#1300)
|
|
360
|
+
apply(size);
|
|
361
|
+
// v2.3.4-beta.4 P1: the box's height is a limit only while it is the box's own. Read again right after the text took
|
|
362
|
+
// its new size (the read lays the page out), a box whose height moved with it, by the same factor, is following its
|
|
363
|
+
// text — a stale stylesheet, or any container that grows with its content: measured next time it would shrink the
|
|
364
|
+
// text 0.5% each refresh, for ever. Then the width alone decides, at once and from then on (until the choice or the
|
|
365
|
+
// stylesheet changes). Cause and effect within this one call: a resize between calls (#2991 review: the box made
|
|
366
|
+
// shorter step by step, where a height-limited size moves in proportion too) never counts. The move must match the
|
|
367
|
+
// expected one closely for its size (#2991 review r2: not ±5% of the whole), and a second probe in the same call —
|
|
368
|
+
// 10% smaller, then back, before any frame is painted — must move it in proportion again: a one-off fixed drop (a
|
|
369
|
+
// scrollbar coming) never does.
|
|
370
|
+
if (!g.follows && was > 0 && before > 0) {
|
|
371
|
+
const height = () => box.clientHeight - padH;
|
|
372
|
+
const moves = (from, to, ratio) => {
|
|
373
|
+
const expect = from * (ratio - 1), got = to - from;
|
|
374
|
+
return got !== 0 && Math.sign(got) === Math.sign(expect) && Math.abs(got - expect) <= Math.max(1.5, 0.25 * Math.abs(expect));
|
|
375
|
+
};
|
|
376
|
+
const after = height();
|
|
377
|
+
if (moves(before, after, size / was)) {
|
|
378
|
+
p.style.fontSize = `${(size * 0.9).toFixed(2)}px`;
|
|
379
|
+
const probed = height();
|
|
380
|
+
p.style.fontSize = `${size.toFixed(2)}px`;
|
|
381
|
+
if (moves(after, probed, 0.9)) {
|
|
382
|
+
g.follows = true;
|
|
383
|
+
const byWidth = sizeFor(byW);
|
|
384
|
+
if (byWidth !== size) apply(byWidth);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
/** Keep a following view on the newest line (a grid taller than the box at the larger sizes scrolls). */
|
|
390
|
+
const toBottom = () => { const box = term.current; if (box) box.scrollTop = box.scrollHeight; };
|
|
391
|
+
const draw = (text) => {
|
|
392
|
+
pre.current.innerHTML = ansiToHtml(text); // escaped by construction: markup only from ansiToHtml's own spans
|
|
393
|
+
paintAnsi(pre.current);
|
|
394
|
+
screenAt.current.drawn = text;
|
|
247
395
|
};
|
|
248
|
-
useEffect(() => { grid.current.key = ""; fit(); }, [density]);
|
|
396
|
+
useEffect(() => { grid.current.key = ""; grid.current.follows = false; fit(); }, [density]);
|
|
397
|
+
// A tab opened before an upgrade: its stylesheet and the View's words read again (healStalePage), then fitted anew.
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
const h = healStalePage(document, (u) => import(u), () => { setWordsRev((n) => n + 1); });
|
|
400
|
+
if (h.css) lease.timeout(() => { grid.current.key = ""; grid.current.follows = false; fit(); }, 300);
|
|
401
|
+
}, []);
|
|
402
|
+
const atBottom = () => { const box = term.current; return !box || box.scrollHeight - box.scrollTop - box.clientHeight <= 2; };
|
|
403
|
+
/** Reading what is shown now (`text`, with or without the scrollback): the refresh only says whether there is more. */
|
|
404
|
+
const startReading = (text, history) => {
|
|
405
|
+
screenAt.current.reading = text; screenAt.current.history = history;
|
|
406
|
+
setNewBelow(false); setMode("reading");
|
|
407
|
+
};
|
|
408
|
+
const grid1 = (r) => {
|
|
409
|
+
const c = Number(r.headers.get("X-Pane-Cols")), rw = Number(r.headers.get("X-Pane-Rows"));
|
|
410
|
+
if (c >= 1 && rw >= 1 && (c !== grid.current.cols || rw !== grid.current.rows)) { grid.current.cols = c; grid.current.rows = rw; grid.current.key = ""; }
|
|
411
|
+
};
|
|
412
|
+
// The person asked for what scrolled off: one bounded read, then reading (or why there is nothing to read).
|
|
413
|
+
const loadEarlier = useRef(null);
|
|
414
|
+
const follow = useRef(null);
|
|
415
|
+
const olderPage = useRef(null);
|
|
249
416
|
useEffect(() => {
|
|
250
417
|
const reads = readStream(lease);
|
|
251
418
|
const refresh = async () => {
|
|
@@ -263,15 +430,130 @@ function Terminal({ name, lease, density, every = 800 }) {
|
|
|
263
430
|
if (!r.ok) { setProblem(r.status === 404 ? tn("paneUnavailable") : r.status === 403 && guestPage() ? tn("guestNeedsTrusted") : tn("paneFailed", { name, status: r.status })); return; }
|
|
264
431
|
const text = await r.text();
|
|
265
432
|
if (!reads.live(token) || !pre.current) return;
|
|
266
|
-
const c = Number(r.headers.get("X-Pane-Cols")), rw = Number(r.headers.get("X-Pane-Rows"));
|
|
267
|
-
if (c >= 1 && rw >= 1 && (c !== grid.current.cols || rw !== grid.current.rows)) { grid.current.cols = c; grid.current.rows = rw; grid.current.key = ""; }
|
|
268
433
|
setProblem(null);
|
|
269
|
-
|
|
270
|
-
|
|
434
|
+
screenAt.current.latest = text;
|
|
435
|
+
// Reading earlier output: nothing is redrawn under the person — the read only says whether there is more.
|
|
436
|
+
if (modeRef.current !== "follow") { if (modeRef.current === "reading") setNewBelow(!sameScreen(screenAt.current.reading, text)); return; }
|
|
437
|
+
// #2969 review (Prism P2): the person is moving a screen taller than its box away from the bottom right now —
|
|
438
|
+
// this is them leaving it, not the latest to follow: read on, nothing redrawn.
|
|
439
|
+
// Reading starts from what is drawn; this newer read (not drawn) is then the new output below.
|
|
440
|
+
if (recentGesture() && !atBottom()) { startReading(screenAt.current.drawn, false); setNewBelow(!sameScreen(screenAt.current.drawn, text)); return; }
|
|
441
|
+
grid1(r);
|
|
442
|
+
draw(text);
|
|
271
443
|
fit();
|
|
444
|
+
toBottom();
|
|
272
445
|
} catch { /* a transient error keeps the last frame */ }
|
|
273
446
|
finally { reads.end(token); }
|
|
274
447
|
};
|
|
448
|
+
loadEarlier.current = async () => {
|
|
449
|
+
// From following, or from reading what was on the screen (the person reached its top): never twice, never over it.
|
|
450
|
+
const from = modeRef.current;
|
|
451
|
+
if (from !== "follow" && !(from === "reading" && !screenAt.current.history)) return;
|
|
452
|
+
if (noScrollbackRef.current) { setHistoryNote(tn("noEarlier"), true); return; } // known: nothing to read
|
|
453
|
+
if (TRANSCRIPT_HISTORY.has(backend)) { await loadTranscript(from); return; }
|
|
454
|
+
const box0 = term.current;
|
|
455
|
+
// Where the person is, measured from the end: the screen's lines stay where they are under the scrollback added.
|
|
456
|
+
const fromEnd = box0 ? box0.scrollHeight - box0.scrollTop : 0;
|
|
457
|
+
const back = () => { setMode(from); if (from === "follow") toBottom(); };
|
|
458
|
+
setMode("loading"); setHistoryNote(tn("loadingEarlier"));
|
|
459
|
+
try {
|
|
460
|
+
const r = await lease.fetch(`/api/pane/${encodeURIComponent(name)}?history=1`);
|
|
461
|
+
if (!lease.current() || modeRef.current !== "loading") return;
|
|
462
|
+
// #2969 review (Prism P2): a scrollback read that failed — or answered without its facts — changes nothing shown.
|
|
463
|
+
if (!r.ok || r.headers.get("X-Pane-History-Lines") == null) { back(); setHistoryNote(r.status === 429 ? null : tn("earlierFailed"), true); return; }
|
|
464
|
+
const text = await r.text();
|
|
465
|
+
if (!lease.current() || modeRef.current !== "loading" || !pre.current) return;
|
|
466
|
+
// A full-screen CLI (the alternate screen, or one that redraws its whole window) leaves nothing above its screen;
|
|
467
|
+
// any other with no history yet has simply not scrolled: say which, keep following.
|
|
468
|
+
if (r.headers.get("X-Pane-Alternate") === "1" || REDRAWS_IN_PLACE.has(backend)) {
|
|
469
|
+
noScrollbackRef.current = true; setNoScrollback(true);
|
|
470
|
+
back(); setHistoryNote(tn("noEarlier"), true); return;
|
|
471
|
+
}
|
|
472
|
+
if (r.headers.get("X-Pane-History-Lines") === "0") { back(); setHistoryNote(tn("nothingEarlier"), true); return; }
|
|
473
|
+
grid1(r);
|
|
474
|
+
const lines = text.split("\n").length, rows = grid.current.rows || 0;
|
|
475
|
+
// #2969 review (Prism P3): new output is judged against what is shown — the scrollback's own last screen — and
|
|
476
|
+
// a screen read while this one was out already counts.
|
|
477
|
+
screenAt.current.reading = text; screenAt.current.history = true;
|
|
478
|
+
setNewBelow(screenAt.current.latest != null && !sameScreen(text, screenAt.current.latest));
|
|
479
|
+
setMode("reading"); setHistoryNote(null);
|
|
480
|
+
draw(text);
|
|
481
|
+
fit();
|
|
482
|
+
const box = term.current;
|
|
483
|
+
if (box) {
|
|
484
|
+
const lineH = lines > 0 ? pre.current.scrollHeight / lines : 16;
|
|
485
|
+
// From following (at the bottom): a few lines up — the earlier output right above. From reading: the same lines.
|
|
486
|
+
box.scrollTop = from === "follow"
|
|
487
|
+
? Math.max(0, box.scrollHeight - box.clientHeight - lineH * Math.min(3, Math.max(1, lines - rows)))
|
|
488
|
+
: Math.max(0, box.scrollHeight - fromEnd);
|
|
489
|
+
}
|
|
490
|
+
} catch { if (lease.current()) { back(); setHistoryNote(tn("earlierFailed"), true); } }
|
|
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
|
+
};
|
|
548
|
+
follow.current = () => {
|
|
549
|
+
if (modeRef.current === "follow") return;
|
|
550
|
+
transcript.current = null;
|
|
551
|
+
setMode("follow"); setNewBelow(false); setHistoryNote(null); screenAt.current.history = false;
|
|
552
|
+
const latest = screenAt.current.latest;
|
|
553
|
+
if (latest != null && pre.current) { draw(latest); fit(); }
|
|
554
|
+
toBottom();
|
|
555
|
+
refresh();
|
|
556
|
+
};
|
|
275
557
|
refresh();
|
|
276
558
|
lease.interval(refresh, every);
|
|
277
559
|
let timer = null;
|
|
@@ -282,8 +564,48 @@ function Terminal({ name, lease, density, every = 800 }) {
|
|
|
282
564
|
ro.observe(term.current);
|
|
283
565
|
lease.hold(() => ro.disconnect());
|
|
284
566
|
}
|
|
567
|
+
// Scrolling up asks for the earlier output; reaching the bottom while reading follows again.
|
|
568
|
+
const box = term.current;
|
|
569
|
+
if (box) {
|
|
570
|
+
// Asking for the earlier output: from following, or from reading the screen as it was — at the top.
|
|
571
|
+
const mayLoad = () => (modeRef.current === "follow" || (modeRef.current === "reading" && !screenAt.current.history)) && box.scrollTop <= 0;
|
|
572
|
+
const gesture = () => { gestureAt.current = performance.now(); };
|
|
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 });
|
|
576
|
+
let touchY = null;
|
|
577
|
+
lease.on(box, "touchstart", (e) => { gesture(); touchY = e.touches && e.touches[0] ? e.touches[0].clientY : null; }, { passive: true });
|
|
578
|
+
lease.on(box, "touchmove", (e) => {
|
|
579
|
+
gesture();
|
|
580
|
+
const y = e.touches && e.touches[0] ? e.touches[0].clientY : null;
|
|
581
|
+
if (touchY == null || y == null) return;
|
|
582
|
+
if (y - touchY > 24 && mayLoad()) { touchY = null; loadEarlier.current(); }
|
|
583
|
+
else if (y - touchY > 24 && nearTop()) { touchY = null; olderPage.current(); }
|
|
584
|
+
}, { passive: true });
|
|
585
|
+
lease.on(box, "pointerdown", gesture, { passive: true }); // the scrollbar, dragged
|
|
586
|
+
lease.on(box, "keydown", (e) => {
|
|
587
|
+
gesture();
|
|
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();
|
|
590
|
+
else if (modeRef.current === "reading" && e.key === "End") { e.preventDefault(); follow.current(); }
|
|
591
|
+
});
|
|
592
|
+
lease.on(box, "scroll", () => {
|
|
593
|
+
if (modeRef.current === "reading") { if (atBottom()) follow.current(); else if (nearTop()) olderPage.current(); return; }
|
|
594
|
+
// #2969 review (Prism P2): a screen taller than its box (a larger Aa), moved up from the bottom by the person:
|
|
595
|
+
// reading what is on it — the refresh no longer pulls them back down.
|
|
596
|
+
if (modeRef.current === "follow" && recentGesture() && !atBottom()) startReading(screenAt.current.drawn, false);
|
|
597
|
+
}, { passive: true });
|
|
598
|
+
}
|
|
285
599
|
}, [lease, name]);
|
|
286
|
-
|
|
600
|
+
const reading = mode === "reading";
|
|
601
|
+
// A full-screen CLI's Log link is a bar above the terminal, not over it: its first row is never covered.
|
|
602
|
+
return html`<div class="v-term-box">
|
|
603
|
+
${noScrollback && logLink ? html`<p class="v-term-logbar"><a class="v-term-log" href=${logLink.href}>${logLink.text}<${Icon} name="forward" size=${14} /></a></p>` : null}
|
|
604
|
+
<div data-trust="panel-content" class=${`v-term${reading ? " is-reading" : ""}`} ref=${term} tabindex="0" aria-label=${tn(reading ? "readingEarlier" : "earlierHint")}>${problem ? html`<p class="v-term-note" role="status">${problem}</p>` : null}<pre ref=${pre} class="v-pre"></pre></div>
|
|
605
|
+
${historyNote ? html`<p class="v-term-history-note" role="status">${historyNote}</p>` : null}
|
|
606
|
+
${mode === "follow" && !noScrollback ? html`<button type="button" class="v-term-earlier btn btn-sm" title=${tn("earlierHint")} onClick=${() => loadEarlier.current && loadEarlier.current()}><${Icon} name="up" size=${14} />${tn("earlier")}</button>` : null}
|
|
607
|
+
${reading ? html`<button type="button" class=${`v-term-latest btn btn-sm${newBelow ? " has-new" : ""}`} onClick=${() => follow.current && follow.current()}>${newBelow ? html`<span class="v-term-new">${tn("newBelow")} · </span>` : null}${tn("jumpLatest")}<${Icon} name="down" size=${14} /></button>` : null}
|
|
608
|
+
</div>`;
|
|
287
609
|
}
|
|
288
610
|
|
|
289
611
|
// The advance of one monospace cell as a fraction of font-size, measured once on this page.
|
|
@@ -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)}`;
|