samagotchi 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -1
- data/README.md +2 -0
- data/bin/chi +3 -0
- data/docs/broadcast.md +129 -0
- data/docs/cli.md +8 -0
- data/docs/configuration.md +2 -0
- data/docs/context.md +37 -0
- data/docs/guardrails.md +14 -4
- data/docs/hooks.md +25 -4
- data/docs/memory.md +3 -0
- data/docs/plugins.md +29 -3
- data/docs/sessions.md +6 -6
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- data/lib/samagotchi/broadcast/recipients.rb +63 -0
- data/lib/samagotchi/broadcast/scope_card.rb +94 -0
- data/lib/samagotchi/broadcast/tags.rb +154 -0
- data/lib/samagotchi/broadcast_command.rb +228 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +7 -6
- data/lib/samagotchi/bundles/coordinator/plugin.rb +72 -4
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +59 -11
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +444 -3
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +22 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -4
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +44 -11
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +32 -12
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +4 -2
- data/lib/samagotchi/bundles/system/self_map.md +4 -0
- data/lib/samagotchi/children_status.rb +2 -16
- data/lib/samagotchi/config.rb +6 -0
- data/lib/samagotchi/engine.rb +29 -5
- data/lib/samagotchi/git_head.rb +24 -0
- data/lib/samagotchi/guardrails/parent_approvals.rb +6 -0
- data/lib/samagotchi/guardrails/rules.rb +15 -4
- data/lib/samagotchi/guardrails/targets.rb +9 -2
- data/lib/samagotchi/guardrails/verdict.rb +4 -3
- data/lib/samagotchi/hooks/registry.rb +11 -5
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/memory_bundle/index_updater.rb +6 -3
- data/lib/samagotchi/note_command.rb +13 -13
- data/lib/samagotchi/note_delivery.rb +42 -0
- data/lib/samagotchi/plugin/context.rb +4 -4
- data/lib/samagotchi/session_commands.rb +1 -0
- data/lib/samagotchi/session_manager.rb +4 -3
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/system_prompt.rb +1 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +25 -2
- data/lib/samagotchi/terminal_ui/status_row.rb +23 -2
- data/lib/samagotchi/terminal_ui.rb +14 -0
- data/lib/samagotchi/tool_activity.rb +14 -3
- data/lib/samagotchi/tool_declarations.rb +8 -4
- data/lib/samagotchi/tools/builtins.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +103 -2
- data/lib/samagotchi/tools/send_note.rb +5 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/capabilities.rb +19 -0
- data/lib/samagotchi/web/public/activity.js +26 -6
- data/lib/samagotchi/web/public/app.js +515 -288
- data/lib/samagotchi/web/public/card.js +12 -3
- data/lib/samagotchi/web/public/command_view.js +2 -3
- data/lib/samagotchi/web/public/families_view.js +171 -0
- data/lib/samagotchi/web/public/format.js +52 -0
- data/lib/samagotchi/web/public/index.html +94 -21
- data/lib/samagotchi/web/public/live_timing.js +154 -0
- data/lib/samagotchi/web/public/live_turn.js +44 -0
- data/lib/samagotchi/web/public/sessions_list.js +228 -32
- data/lib/samagotchi/web/public/strip.js +5 -2
- data/lib/samagotchi/web/public/turn_html.js +107 -0
- data/lib/samagotchi/web/public/turn_model.js +5 -4
- data/lib/samagotchi/web/public/turn_view.js +43 -129
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +166 -307
- data/lib/samagotchi/worker_inbound.rb +251 -0
- data/lib/samagotchi/worker_wakes.rb +120 -0
- metadata +15 -1
|
@@ -81,14 +81,23 @@ export function cardPlace(userBubbles, card) {
|
|
|
81
81
|
return recapPlace(userBubbles, card?.turns_since);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Whether a notice stands in for something this page already shows: its
|
|
85
|
+
// fallback_for names a capability the server says the page has (the
|
|
86
|
+
// session JSON's capabilities, e.g. { display: true } when answers render
|
|
87
|
+
// as markdown, so a hook's links are links). Such a notice isn't drawn.
|
|
88
|
+
export function shownElsewhere(notice, capabilities) {
|
|
89
|
+
return !!notice?.fallback_for && capabilities?.[notice.fallback_for] === true;
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
// A snapshot's cards: the ones placed in the history, the running turn's
|
|
85
93
|
// (a row of its current step once the turn is drawn), and the ones that
|
|
86
94
|
// came during the running turn but aren't its own (an anytime command's,
|
|
87
95
|
// such as /btw: after the running turn once it is drawn, as they came).
|
|
88
96
|
// The running turn's own rows (a hook's notice, a retry) are left out:
|
|
89
|
-
// they come with the turn's parts (snapshotEvents).
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
// they come with the turn's parts (snapshotEvents). So is a notice the
|
|
98
|
+
// page shows elsewhere (shownElsewhere).
|
|
99
|
+
export function splitSnapshotCards(cards, capabilities = null) {
|
|
100
|
+
const list = (Array.isArray(cards) ? cards : []).filter((c) => !shownElsewhere(c, capabilities));
|
|
92
101
|
return {
|
|
93
102
|
placed: list.filter((c) => !c.current && !c.during),
|
|
94
103
|
current: list.filter((c) => c.current && isCard(c)),
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// A tool call's full command in a richer UI than the TUI's params line:
|
|
2
2
|
// the server's view (ToolView: an execute/task_create's command uncut up to
|
|
3
|
-
// 8,000 chars, the call's cwd: argument). Pure HTML builders
|
|
4
|
-
//
|
|
5
|
-
// (reloadRowHtml), so the two can't differ.
|
|
3
|
+
// 8,000 chars, the call's cwd: argument). Pure HTML builders for the tool
|
|
4
|
+
// row (turn_html.js toolRowHtml, live and reloaded alike).
|
|
6
5
|
import { copyButtonHtml } from "./copy.js";
|
|
7
6
|
import { escapeHtml } from "./format.js";
|
|
8
7
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// A parent's card with its delegates folded in (docs/sessions.md, the web):
|
|
2
|
+
// the chip that opens the family's list, and the list's rows. Pure string
|
|
3
|
+
// builders; app.js places them (inline in all sessions, a popover over the
|
|
4
|
+
// strip) and binds the clicks.
|
|
5
|
+
import { escapeHtml, ownerBadge, previewForCard, relativeTime } from "./format.js";
|
|
6
|
+
import { familySummary, stoppedBadge, waitingBadge } from "./sessions_list.js";
|
|
7
|
+
|
|
8
|
+
// The chip in a parent's meta row: "▸ 3 delegates · ● 1 live · 1 waiting"
|
|
9
|
+
// (a waiting one in the attention colour), its tooltip the delegates by
|
|
10
|
+
// state. "" for a family with no members.
|
|
11
|
+
// @param open whether the list is open (aria-expanded; the arrow turns)
|
|
12
|
+
export function familyChipHtml(f, { open = false } = {}) {
|
|
13
|
+
const sum = familySummary(f);
|
|
14
|
+
if (!sum) return "";
|
|
15
|
+
const waiting = sum.counts.waiting;
|
|
16
|
+
const liveHtml = sum.live ? `<span class="fc-live"> · <span class="live-dot" aria-hidden="true"></span>${sum.live} live</span>` : "";
|
|
17
|
+
const waitingHtml = waiting ? ` · <span class="fc-waiting">${waiting} waiting</span>` : "";
|
|
18
|
+
return `<button type="button" class="family-chip${waiting ? " waiting" : ""}" aria-expanded="${open ? "true" : "false"}" title="${escapeHtml(sum.title)}"><span class="fc-arrow" aria-hidden="true">▸</span>${sum.total} ${sum.total === 1 ? "delegate" : "delegates"}${liveHtml}${waitingHtml}</button>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The family's list: one row per member, depth first, indented one step
|
|
22
|
+
// per depth (--depth): status dot, preview, time, the waiting / stopped
|
|
23
|
+
// badge, the owner badge, archived. A row's click opens that session.
|
|
24
|
+
// @param activeId the open session's id (its row is .active)
|
|
25
|
+
// @param matchIds a search's matching members (.match; the rest .dim), or
|
|
26
|
+
// null without a search
|
|
27
|
+
// @param archivable an archive button per row (all sessions, not in select mode)
|
|
28
|
+
export function familyRowsHtml(f, { activeId = null, matchIds = null, archivable = false, now = Date.now() } = {}) {
|
|
29
|
+
const rows = f.members.map(({ session: s, depth }) => {
|
|
30
|
+
const cls = ["family-row"];
|
|
31
|
+
if (activeId && s.id === activeId) cls.push("active");
|
|
32
|
+
if (matchIds) cls.push(matchIds.has(s.id) ? "match" : "dim");
|
|
33
|
+
if (s.archived) cls.push("archived");
|
|
34
|
+
const ts = s.updated_at || s.created_at || "";
|
|
35
|
+
const waiting = waitingBadge(s, { inFamily: true });
|
|
36
|
+
const stopped = waiting ? null : stoppedBadge(s);
|
|
37
|
+
const owner = ownerBadge(s.owner, s.id);
|
|
38
|
+
const ago = relativeTime(ts, now);
|
|
39
|
+
const dotWord = waiting ? waiting.title : s.status;
|
|
40
|
+
const parts = [
|
|
41
|
+
`<span class="status dot ${escapeHtml(s.status || "")}${waiting ? " waiting" : ""}" title="${escapeHtml(dotWord || "")}" aria-label="${escapeHtml(dotWord || "")}"></span>`,
|
|
42
|
+
`<span class="fr-preview">${escapeHtml(previewForCard(s))}</span>`,
|
|
43
|
+
waiting ? `<span class="attn attn-${waiting.kind}" title="${escapeHtml(waiting.title)}">${escapeHtml(waiting.text)}</span>` : "",
|
|
44
|
+
stopped ? `<span class="stopped-badge" title="${escapeHtml(stopped.title)}">${escapeHtml(stopped.text)}</span>` : "",
|
|
45
|
+
ago ? `<span class="time">${escapeHtml(ago)}</span>` : "",
|
|
46
|
+
owner ? `<span class="owner ${owner.kind}" title="${escapeHtml(owner.title)}">${escapeHtml(owner.text)}</span>` : "",
|
|
47
|
+
s.archived ? `<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>` : "",
|
|
48
|
+
].join("");
|
|
49
|
+
const short = escapeHtml(s.short_id || String(s.id).slice(0, 8));
|
|
50
|
+
const arch = !archivable ? "" : s.archived
|
|
51
|
+
? `<button type="button" class="card-arch" data-arch="${escapeHtml(s.id)}" title="Unarchive: back to the lists" aria-label="Unarchive session ${short}"></button>`
|
|
52
|
+
: `<button type="button" class="card-arch" data-arch="${escapeHtml(s.id)}" title="Archive: hide from the lists, keep for good" aria-label="Archive session ${short}"></button>`;
|
|
53
|
+
return `<div class="${cls.join(" ")}" data-id="${escapeHtml(s.id)}" style="--depth:${depth}" role="listitem"><button type="button" class="fr-open" title="${escapeHtml(`${s.id} · ${s.status || ""}`)}">${parts}</button>${arch}</div>`;
|
|
54
|
+
});
|
|
55
|
+
return `<div class="family-rows" role="list">${rows.join("")}</div>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The widest window index.html lays out as a phone (@media (max-width:600px)).
|
|
59
|
+
export const PHONE_MAX_PX = 600;
|
|
60
|
+
|
|
61
|
+
// The strip's family popover: one #familyPop in body (position: fixed, so
|
|
62
|
+
// the strip's sideways scroll on a phone doesn't clip it), under the chip
|
|
63
|
+
// it was opened from and as wide as that card (300 px at least, inside the
|
|
64
|
+
// window; on a phone, the window less 16 px gutters). The strip draws again on every hub event: refresh() then finds
|
|
65
|
+
// the new chip for the same head, rewrites the rows only when they changed,
|
|
66
|
+
// and closes when the family is gone.
|
|
67
|
+
// @param rowsFor (headId) → the rows' html, or null when the family has no
|
|
68
|
+
// members (any more)
|
|
69
|
+
// @param anchorFor (headId) → its chip in the strip, or null
|
|
70
|
+
// @param onPick (id) a row's click
|
|
71
|
+
// @param onClose () after it closed (the strip's held order catches up)
|
|
72
|
+
export function createFamilyPop({ doc, win, rowsFor, anchorFor, onPick, onClose = () => {} }) {
|
|
73
|
+
let el = null;
|
|
74
|
+
let headId = null;
|
|
75
|
+
let html = "";
|
|
76
|
+
|
|
77
|
+
const element = () => {
|
|
78
|
+
if (el) return el;
|
|
79
|
+
el = doc.createElement("div");
|
|
80
|
+
el.id = "familyPop";
|
|
81
|
+
el.className = "family-pop";
|
|
82
|
+
el.hidden = true;
|
|
83
|
+
el.addEventListener("click", (e) => {
|
|
84
|
+
const row = e.target?.closest?.(".family-row[data-id]");
|
|
85
|
+
if (row) onPick(row.dataset.id);
|
|
86
|
+
});
|
|
87
|
+
doc.body.appendChild(el);
|
|
88
|
+
return el;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const place = (anchor) => {
|
|
92
|
+
const chip = anchor.getBoundingClientRect();
|
|
93
|
+
const card = (anchor.closest(".card") || anchor).getBoundingClientRect();
|
|
94
|
+
const vw = win.innerWidth;
|
|
95
|
+
// A phone (index.html's 600 px layout): the width minus 16 px gutters.
|
|
96
|
+
const phone = vw <= PHONE_MAX_PX;
|
|
97
|
+
const width = phone ? vw - 32 : Math.max(300, Math.round(card.width));
|
|
98
|
+
const left = phone ? 16 : Math.max(8, Math.min(Math.round(card.left), vw - width - 8));
|
|
99
|
+
el.style.top = `${Math.round(chip.bottom + 6)}px`;
|
|
100
|
+
el.style.left = `${left}px`;
|
|
101
|
+
el.style.width = `${width}px`;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Draws the open family again: false (and closed) when it is gone.
|
|
105
|
+
const draw = () => {
|
|
106
|
+
const rows = rowsFor(headId);
|
|
107
|
+
const anchor = rows == null ? null : anchorFor(headId);
|
|
108
|
+
if (!anchor) {
|
|
109
|
+
close();
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (rows !== html) {
|
|
113
|
+
el.innerHTML = rows;
|
|
114
|
+
html = rows;
|
|
115
|
+
}
|
|
116
|
+
anchor.setAttribute("aria-expanded", "true");
|
|
117
|
+
place(anchor);
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function open(id) {
|
|
122
|
+
if (headId && headId !== id) close();
|
|
123
|
+
headId = id;
|
|
124
|
+
element().hidden = false;
|
|
125
|
+
html = "";
|
|
126
|
+
draw();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function close() {
|
|
130
|
+
if (!headId) return;
|
|
131
|
+
anchorFor(headId)?.setAttribute("aria-expanded", "false");
|
|
132
|
+
headId = null;
|
|
133
|
+
html = "";
|
|
134
|
+
el.hidden = true;
|
|
135
|
+
el.innerHTML = "";
|
|
136
|
+
onClose();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
open,
|
|
141
|
+
close,
|
|
142
|
+
toggle: (id) => (headId === id ? close() : open(id)),
|
|
143
|
+
isOpen: () => headId != null,
|
|
144
|
+
openHead: () => headId,
|
|
145
|
+
// After the strip drew again.
|
|
146
|
+
refresh: () => { if (headId) draw(); },
|
|
147
|
+
// The page scrolled or resized under it.
|
|
148
|
+
reposition: () => {
|
|
149
|
+
const anchor = headId && anchorFor(headId);
|
|
150
|
+
if (anchor) place(anchor);
|
|
151
|
+
},
|
|
152
|
+
// Esc closes it, before any other Esc handler (app.js listens in the
|
|
153
|
+
// capture phase): true when it did.
|
|
154
|
+
onKeydown(e) {
|
|
155
|
+
if (!headId || e.key !== "Escape") return false;
|
|
156
|
+
const id = headId;
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
e.stopImmediatePropagation();
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
close();
|
|
161
|
+
anchorFor(id)?.focus(); // the chip as drawn after close() (a held strip catches up)
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
164
|
+
// A pointer down outside it and its chip closes it.
|
|
165
|
+
onPointerDown(e) {
|
|
166
|
+
if (!headId || el.contains(e.target)) return;
|
|
167
|
+
if (anchorFor(headId)?.contains(e.target)) return;
|
|
168
|
+
close();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { waitingOn } from "./notify.js";
|
|
2
|
+
|
|
1
3
|
export const PREVIEW_CHARS = 40;
|
|
2
4
|
export const FIRST_PREVIEW_CHARS = 80;
|
|
3
5
|
|
|
@@ -141,6 +143,56 @@ export function delegatedBy(parentId, parent = null) {
|
|
|
141
143
|
return { text: `\u21B3 ${short}`, title: `delegated by session ${short}${what ? `: ${what}` : ""}` };
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
// A child's state for the parent's children chip, decided in
|
|
147
|
+
// ChildrenStatus's order: waiting on the user (a question, an approval, a
|
|
148
|
+
// card), running, stopped, failed, done (the last turn completed), idle.
|
|
149
|
+
// The hub has no reply files, so "done" doesn't say whether the parent was
|
|
150
|
+
// given the reply ("not reported" is the TUI's and /children's).
|
|
151
|
+
const CHILD_STATES = ["running", "waiting", "done", "failed", "stopped", "idle"];
|
|
152
|
+
function childState(s) {
|
|
153
|
+
if (waitingOn(s)) return "waiting";
|
|
154
|
+
if (s.status === "running") return "running";
|
|
155
|
+
if (s.status === "stopped") return "stopped";
|
|
156
|
+
const outcome = s.last_turn?.outcome;
|
|
157
|
+
if (s.status === "error" || outcome === "failed") return "failed";
|
|
158
|
+
if (outcome === "completed") return "done";
|
|
159
|
+
return "idle";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Delegates by state: the one counter behind the parent's `⑂ N` chip (the
|
|
163
|
+
// info bar) and its card's family chip. live: how many a worker runs now
|
|
164
|
+
// (owner "worker", the cards' green badge), whatever their state.
|
|
165
|
+
// @param kids the delegates' summaries
|
|
166
|
+
// @return null for none, else {total, counts, live, text, waitingText,
|
|
167
|
+
// title}; waitingText ("2 waiting") is for the attention colour, null when
|
|
168
|
+
// none waits
|
|
169
|
+
export function delegatesSummary(kids) {
|
|
170
|
+
const list = (kids || []).filter(Boolean);
|
|
171
|
+
if (!list.length) return null;
|
|
172
|
+
const counts = Object.fromEntries(CHILD_STATES.map((k) => [k, 0]));
|
|
173
|
+
list.forEach((s) => { counts[childState(s)] += 1; });
|
|
174
|
+
const parts = CHILD_STATES.filter((k) => counts[k]).map((k) => `${counts[k]} ${k}`);
|
|
175
|
+
const total = list.length;
|
|
176
|
+
return {
|
|
177
|
+
total,
|
|
178
|
+
counts,
|
|
179
|
+
live: list.filter((s) => s.owner === "worker").length,
|
|
180
|
+
text: `\u2442 ${total}`,
|
|
181
|
+
waitingText: counts.waiting ? `${counts.waiting} waiting` : null,
|
|
182
|
+
title: `${total} ${total === 1 ? "delegate" : "delegates"}: ${parts.join(" \u00b7 ")}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The parent's `⑂ N` chip (the info bar): its delegates in the list, by
|
|
187
|
+
// state (delegatesSummary). Forks (delegate false) and archived children are
|
|
188
|
+
// left out, as in the TUI's segment (ChildrenStatus.counts).
|
|
189
|
+
// @param parent the parent's summary
|
|
190
|
+
// @param sessions the page's list (allSessions: archived ones too)
|
|
191
|
+
export function childrenSummary(parent, sessions) {
|
|
192
|
+
if (!parent?.id) return null;
|
|
193
|
+
return delegatesSummary((sessions || []).filter((s) => s && s.parent_id === parent.id && s.delegate && !s.archived && s.id !== parent.id));
|
|
194
|
+
}
|
|
195
|
+
|
|
144
196
|
// Whether the info bar offers "stop": only while a worker runs the session
|
|
145
197
|
// (the list says so, or this page streams from one a send just woke). A
|
|
146
198
|
// stopped or idle-exited session has nothing to stop, and a chi REPL's is
|
|
@@ -5,23 +5,27 @@
|
|
|
5
5
|
<title>Chi</title>
|
|
6
6
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
7
|
<style>
|
|
8
|
-
:root{color-scheme:dark;--bg:#0b0d11;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#7aa5ff;--accent-strong:#1f6feb;--accent-bg:#1b2336;--accent-ring:rgba(122,165,255,.18);--primary:#2a68d1;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#223150;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);--narr:color-mix(in srgb,var(--accent) 38%,var(--text-muted));
|
|
8
|
+
:root{color-scheme:dark;--bg:#0b0d11;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#7aa5ff;--accent-strong:#1f6feb;--accent-bg:#1b2336;--accent-ring:rgba(122,165,255,.18);--primary:#2a68d1;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#223150;--pick-border:#79c0ff;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);--narr:color-mix(in srgb,var(--accent) 38%,var(--text-muted));
|
|
9
9
|
--glass-bg:color-mix(in srgb,var(--bg-panel) 70%,transparent);--glass-border:rgba(255,255,255,.08);--glass-highlight:rgba(255,255,255,.05);--glass-shadow:0 8px 30px rgba(0,0,0,.35);
|
|
10
10
|
--diff-add-bg:rgba(63,185,80,.14);--diff-add-text:#aff5b4;--diff-del-bg:rgba(248,81,73,.14);--diff-del-text:#ffc1bc;
|
|
11
11
|
--code-text:#c9d1d9;--code-keyword:#ff7b72;--code-string:#a5d6ff;--code-comment:#8b949e;--code-constant:#79c0ff;--code-func:#d2a8ff;--code-type:#ffa657;
|
|
12
12
|
--card-hover-border:rgba(255,255,255,.14);--on-solid:#fff;--on-warn:#fff;--text-hover:#fff;--badge-bg:rgba(255,255,255,.08);--badge-ring:rgba(255,255,255,.18);--shadow-strong:rgba(0,0,0,.5);--backdrop:rgba(0,0,0,.85);--toast-shadow:0 6px 24px rgba(0,0,0,.25)}
|
|
13
13
|
/* Light palette: follows the system setting. :not([data-theme="dark"]) leaves room for a later in-page toggle (data-theme on <html>); there is none yet. Each palette sets color-scheme, so native radios, inputs and scrollbars follow it. */
|
|
14
|
-
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){color-scheme:light;--bg:#f5f6f8;--bg-panel:#ffffff;--bg-inset:#f1f3f6;--bg-raised:#e8ebf0;--bg-hover:#eceff4;--border:#dde1e7;--border-strong:#c3c9d2;--text:#1f2328;--text-bright:#0d1117;--text-2:#2f363d;--text-3:#475160;--text-muted:#57606a;--text-faint:#636c76;--accent:#0b5cd5;--accent-strong:#0550ae;--accent-bg:#e2ecfd;--accent-ring:rgba(11,92,213,.2);--primary:#1f6feb;--ok:#1a7f37;--ok-text:#116329;--ok-bg:#dafbe1;--warn:#9a6700;--warn-text:#7d4e00;--warn-bright:#875c00;--warn-bg:#fff6cc;--warn-border:#d4a72c;--danger:#cf222e;--danger-bg:#cf222e;--danger-text:#a40e26;--danger-bg-dim:#ffebe9;--think:#8250df;--think-text:#6639ba;--accent-border:#b8cef5;
|
|
14
|
+
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){color-scheme:light;--bg:#f5f6f8;--bg-panel:#ffffff;--bg-inset:#f1f3f6;--bg-raised:#e8ebf0;--bg-hover:#eceff4;--border:#dde1e7;--border-strong:#c3c9d2;--text:#1f2328;--text-bright:#0d1117;--text-2:#2f363d;--text-3:#475160;--text-muted:#57606a;--text-faint:#636c76;--accent:#0b5cd5;--accent-strong:#0550ae;--accent-bg:#e2ecfd;--accent-ring:rgba(11,92,213,.2);--primary:#1f6feb;--ok:#1a7f37;--ok-text:#116329;--ok-bg:#dafbe1;--warn:#9a6700;--warn-text:#7d4e00;--warn-bright:#875c00;--warn-bg:#fff6cc;--warn-border:#d4a72c;--danger:#cf222e;--danger-bg:#cf222e;--danger-text:#a40e26;--danger-bg-dim:#ffebe9;--think:#8250df;--think-text:#6639ba;--accent-border:#b8cef5;--pick-border:#0969da;
|
|
15
15
|
--glass-border:rgba(31,35,40,.10);--glass-highlight:rgba(255,255,255,.7);--glass-shadow:0 8px 30px rgba(31,35,40,.10);
|
|
16
16
|
--diff-add-bg:rgba(26,127,55,.12);--diff-add-text:#116329;--diff-del-bg:rgba(207,34,46,.10);--diff-del-text:#82071e;
|
|
17
17
|
--code-text:#1f2328;--code-keyword:#cf222e;--code-string:#0a3069;--code-comment:#57606a;--code-constant:#0550ae;--code-func:#8250df;--code-type:#953800;
|
|
18
18
|
--card-hover-border:rgba(31,35,40,.18);--on-solid:#fff;--on-warn:#1f2328;--text-hover:#0d1117;--badge-bg:rgba(31,35,40,.06);--badge-ring:rgba(31,35,40,.15);--shadow-strong:rgba(31,35,40,.18);--backdrop:rgba(13,17,23,.85);--toast-shadow:0 6px 24px rgba(31,35,40,.14)}}
|
|
19
|
+
/* Icon masks shared by the phone info bar and the all view's cards (the stroke only shapes the mask; the icon takes currentColor). */
|
|
20
|
+
:root{--icon-archive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M6.5 9h3'/%3E%3C/svg%3E");--icon-unarchive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M8 11.5V8.25M6.5 9.75 8 8.25l1.5 1.5'/%3E%3C/svg%3E")}
|
|
19
21
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
20
22
|
body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:radial-gradient(1100px 700px at 8% 0%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 70%),radial-gradient(900px 700px at 100% 100%,color-mix(in srgb,var(--think) 9%,transparent),transparent 70%),var(--bg);background-attachment:fixed;color:var(--text);height:100vh;display:flex;flex-direction:column}
|
|
21
23
|
/* Glass: the composer card, the strip cards, the all-sessions cards and the question cards (by selector, so app.js is untouched). */
|
|
22
24
|
#dock,.turn-stage,.dock-card,#topStrip .card,#allList .card,.bubble.question,.bubble.plugin-card{background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
|
|
23
25
|
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:var(--card-hover-border)}
|
|
24
26
|
#topStrip .card.active,#allList .card.active{background:color-mix(in srgb,var(--accent-bg) 75%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
|
|
27
|
+
/* A parent whose folded family holds the open session: a quieter mark than .active. */
|
|
28
|
+
#topStrip .card.contains-active,#allList .card.contains-active{border-color:color-mix(in srgb,var(--accent) 22%,transparent)}
|
|
25
29
|
/* A session that waits on the user (a question, an approval, a card with actions): the warning colour of its question card, also when active or hovered. */
|
|
26
30
|
#topStrip .card.waiting,#allList .card.waiting{border-color:color-mix(in srgb,var(--warn) 75%,transparent)}
|
|
27
31
|
#topStrip .card.waiting:hover,#allList .card.waiting:hover{border-color:var(--warn)}
|
|
@@ -727,6 +731,8 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
727
731
|
#infoBar .meta .status{order:-1}
|
|
728
732
|
/* After the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
729
733
|
#infoBar .meta .mem-chip{display:inline;order:-1}
|
|
734
|
+
/* A parent's children chip too: its waiting part is what to see. */
|
|
735
|
+
#infoBar .meta .children-chip{order:-1}
|
|
730
736
|
/* The chips get a second line of their own (only when there are some), scrolling sideways. */
|
|
731
737
|
#infoBar:has(#contextChips:not([hidden])){flex-wrap:wrap;row-gap:4px}
|
|
732
738
|
#contextChips{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}
|
|
@@ -738,8 +744,8 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
738
744
|
/* Icon-only buttons (the words stay as the accessible name, the title says more): the meta gets the width. */
|
|
739
745
|
#infoBar>button{font-size:0;line-height:0;padding:5px 7px}
|
|
740
746
|
#infoBar>button::before{content:"";display:block;width:14px;height:14px;background:currentColor;-webkit-mask:var(--icon) center/contain no-repeat;mask:var(--icon) center/contain no-repeat}
|
|
741
|
-
#infoBar #infoArchiveBtn{--icon:
|
|
742
|
-
#infoBar #infoArchiveBtn.archived{--icon:
|
|
747
|
+
#infoBar #infoArchiveBtn{--icon:var(--icon-archive)}
|
|
748
|
+
#infoBar #infoArchiveBtn.archived{--icon:var(--icon-unarchive)}
|
|
743
749
|
#infoBar #infoStopBtn{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v5.5M4.6 4.4a5 5 0 1 0 6.8 0'/%3E%3C/svg%3E")}
|
|
744
750
|
#infoBar #infoDeleteBtn{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.5h11M6 4.5V2.75h4V4.5M4 4.5l.7 8.6a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9l.7-8.6M6.75 7.25v4.5M9.25 7.25v4.5'/%3E%3C/svg%3E")}
|
|
745
751
|
}
|
|
@@ -803,6 +809,46 @@ body.has-session #scope .scope-where{display:none}
|
|
|
803
809
|
.card .parent{font-family:ui-monospace,monospace;white-space:nowrap;color:var(--text-3)}
|
|
804
810
|
#infoBar .meta a.parent{text-decoration:none}
|
|
805
811
|
#infoBar .meta a.parent:hover{color:var(--accent)}
|
|
812
|
+
/* The strip's family popover (in body, position: fixed: the strip's sideways scroll on a phone would clip it), glass as the cards. */
|
|
813
|
+
#familyPop{position:fixed;z-index:40;max-height:50vh;overflow:auto;padding:6px;border-radius:10px;background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
|
|
814
|
+
#familyPop[hidden]{display:none}
|
|
815
|
+
#familyPop .family-rows{border-top:0;margin:0;padding-top:0}
|
|
816
|
+
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){#familyPop{background:var(--bg-panel)}}
|
|
817
|
+
@media (prefers-reduced-transparency:reduce){#familyPop{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}}
|
|
818
|
+
body.no-glass #familyPop{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
819
|
+
/* A parent's family chip: its delegates folded into its card; a click opens or closes their list. */
|
|
820
|
+
/* A card with a family chip lets its meta row wrap: the chip goes to a line of its own when the card is narrow. */
|
|
821
|
+
.card .meta:has(.family-chip){flex-wrap:wrap;row-gap:4px}
|
|
822
|
+
.card .family-chip{font-family:ui-monospace,monospace;font-size:11px;line-height:1.4;white-space:nowrap;flex-shrink:0;padding:1px 7px;border-radius:999px;border:1px solid var(--border-strong);background:transparent;color:var(--text-3);cursor:pointer}
|
|
823
|
+
.card .family-chip:hover:not(:disabled){filter:none;color:var(--accent);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
824
|
+
.card .family-chip.waiting{color:var(--warn-text);background:var(--warn-bg);border-color:var(--warn-border)}
|
|
825
|
+
.card .family-chip.waiting:hover:not(:disabled){color:var(--warn-bright);border-color:var(--warn);background:var(--warn-bg)}
|
|
826
|
+
.family-chip .fc-arrow{display:inline-block;margin-right:4px;transition:transform .15s ease}
|
|
827
|
+
.family-chip[aria-expanded="true"] .fc-arrow{transform:rotate(90deg)}
|
|
828
|
+
.family-chip .live-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--ok);margin-right:3px;vertical-align:1px}
|
|
829
|
+
.family-chip .fc-waiting{font-family:system-ui,sans-serif;font-weight:600}
|
|
830
|
+
/* The family's list: one compact row per delegate, indented per depth. */
|
|
831
|
+
.family-rows{display:flex;flex-direction:column;gap:1px;border-top:1px solid var(--border);margin:2px -6px 0;padding-top:4px;cursor:default}
|
|
832
|
+
.family-row{display:flex;align-items:center;border-radius:6px;padding-left:calc((var(--depth,1) - 1) * 14px)}
|
|
833
|
+
.family-row:hover{background:var(--bg-hover)}
|
|
834
|
+
.family-row .fr-open{flex:1;min-width:0;display:flex;align-items:center;gap:6px;padding:6px;background:transparent;color:var(--text-2);border-radius:6px;font-size:12px;line-height:1.3;text-align:left}
|
|
835
|
+
.family-row .fr-open:hover:not(:disabled){filter:none}
|
|
836
|
+
.family-row .fr-preview{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
837
|
+
.family-row .time{font-size:11px;color:var(--text-muted);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
838
|
+
.family-row.active{background:var(--accent-bg)}
|
|
839
|
+
.family-row.active .fr-preview{color:var(--text-bright)}
|
|
840
|
+
.family-row.dim{opacity:.5}
|
|
841
|
+
.family-row.match .fr-preview{color:var(--text-bright);font-weight:600}
|
|
842
|
+
.family-row.archived{opacity:.6}
|
|
843
|
+
.family-row .card-arch{flex-shrink:0;padding:4px 7px;margin-right:2px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);opacity:0;transition:opacity .15s}
|
|
844
|
+
.family-row:hover .card-arch,.family-row .card-arch:focus-visible{opacity:1}
|
|
845
|
+
.card .family-row .card-arch::before{-webkit-mask-image:var(--icon-archive);mask-image:var(--icon-archive)}
|
|
846
|
+
.card .family-row.archived .card-arch::before{-webkit-mask-image:var(--icon-unarchive);mask-image:var(--icon-unarchive)}
|
|
847
|
+
@media (hover:none){.family-row .card-arch{opacity:1}}
|
|
848
|
+
.children-chip .kids-waiting{font-family:system-ui,sans-serif;font-weight:600;color:var(--warn-text)}
|
|
849
|
+
#infoBar .meta a.children-chip{text-decoration:none;font-family:ui-monospace,monospace;white-space:nowrap}
|
|
850
|
+
#infoBar .meta a.children-chip:hover{color:var(--accent)}
|
|
851
|
+
#infoBar .meta a.children-chip.waiting{background:var(--warn-bg);border:1px solid var(--warn-border);padding:1px 6px}
|
|
806
852
|
.scope-error{grid-column:1/-1;font-size:13px;color:var(--warn-text)}
|
|
807
853
|
.strip-toggle{font-size:11px;padding:3px 10px;background:transparent;color:var(--text-muted);border:1px solid var(--border);border-radius:999px}
|
|
808
854
|
.strip-toggle:hover{color:var(--text-2);border-color:var(--border-strong)}
|
|
@@ -835,10 +881,22 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
835
881
|
#allPath[hidden]{display:none}
|
|
836
882
|
#allPath code{font-family:ui-monospace,monospace;background:var(--bg-hover);padding:1px 4px;border-radius:4px}
|
|
837
883
|
#allHead .count{font-size:11px;color:var(--text-muted);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
884
|
+
#allHead #selectBtn{padding:6px 10px;font-size:12px}
|
|
885
|
+
#selectBar{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
|
886
|
+
#selectBar[hidden],#selectBtn[hidden],#selectBar [hidden]{display:none}
|
|
887
|
+
#selectBar button{padding:6px 10px;font-size:12px;white-space:nowrap}
|
|
888
|
+
#selectBar button:disabled{opacity:.5;cursor:default}
|
|
889
|
+
#selectCount{font-size:12px;color:var(--text-2);white-space:nowrap;font-variant-numeric:tabular-nums;margin-right:2px}
|
|
890
|
+
/* Select mode: a checkbox at the card's top left; a pick wears its own outline (.active is the open session's). */
|
|
891
|
+
#allView.selecting #allList .card{padding-left:38px;user-select:none}
|
|
892
|
+
#allList .card-pick{position:absolute;top:13px;left:13px;width:16px;height:16px;margin:0;accent-color:var(--pick-border);cursor:pointer}
|
|
893
|
+
#allList .card.picked,#allList .card.picked:hover{border-color:var(--pick-border);box-shadow:inset 0 0 0 1px var(--pick-border),var(--glass-shadow);background:color-mix(in srgb,var(--pick-border) 9%,var(--glass-bg))}
|
|
894
|
+
.card .skip-reason{font-size:11px;line-height:1.35;color:var(--warn-text);display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;word-break:break-word}
|
|
895
|
+
@media(max-width:600px){#allHead{flex-wrap:wrap}#allHead input#filter{flex-basis:140px}#allView.selecting #allHead .count{display:none}#selectBar{flex-basis:100%;order:5}#selectCount{flex-basis:100%}#allList .card-pick{width:20px;height:20px;top:12px;left:11px}}
|
|
838
896
|
#allHead .include-archived{display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text-muted);white-space:nowrap;cursor:pointer;user-select:none}
|
|
839
897
|
.card.archived{opacity:.6}
|
|
840
898
|
.card.archived:hover{opacity:.85}
|
|
841
|
-
.card .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
899
|
+
.card .archived-badge,.family-row .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
842
900
|
#toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
843
901
|
#toast[hidden]{display:none}
|
|
844
902
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
@@ -855,10 +913,17 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
855
913
|
.card{padding:12px;border-radius:10px;background:var(--bg-panel);border:1px solid var(--border);cursor:pointer;display:flex;flex-direction:column;gap:6px;min-height:72px}
|
|
856
914
|
.card:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
857
915
|
#allList .card{position:relative}
|
|
858
|
-
|
|
859
|
-
#allList .card
|
|
916
|
+
/* A card's tools in the all view: archive (or unarchive) and delete, shown on hover, always under hover:none. */
|
|
917
|
+
#allList .card-tools{position:absolute;top:6px;right:6px;display:flex;gap:2px}
|
|
918
|
+
.card-tools button{padding:2px 7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);font-size:12px;line-height:1.2;cursor:pointer;opacity:0;transition:opacity .15s}
|
|
919
|
+
#allList .card:hover .card-tools button, .card-tools button:focus-visible{opacity:1}
|
|
860
920
|
.card-del:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
861
|
-
|
|
921
|
+
.card-arch{display:flex;align-items:center}
|
|
922
|
+
.card-arch::before{content:"";display:block;width:14px;height:14px;background:currentColor;-webkit-mask:var(--icon-archive) center/contain no-repeat;mask:var(--icon-archive) center/contain no-repeat}
|
|
923
|
+
.card.archived .card-arch::before{-webkit-mask-image:var(--icon-unarchive);mask-image:var(--icon-unarchive)}
|
|
924
|
+
.card-arch:hover{color:var(--accent);background:var(--accent-bg);border-color:var(--accent-border)}
|
|
925
|
+
#allList .card:has(.card-tools) .preview{padding-right:56px}
|
|
926
|
+
@media (hover:none){.card-tools button{opacity:1}}
|
|
862
927
|
.card.active{background:var(--accent-bg);border-color:var(--accent-border)}
|
|
863
928
|
.card .preview{font-size:14px;line-height:1.4;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
864
929
|
.card .time{font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
@@ -868,25 +933,25 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
868
933
|
.card .recap{font-size:12px;line-height:1.4;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:-2px 0 2px}
|
|
869
934
|
.card .meta{font-size:11px;color:var(--text-muted);display:flex;gap:6px;align-items:center}
|
|
870
935
|
/* A card's status is a dot (its word is the tooltip); the .status.* pills below are the info bar's. */
|
|
871
|
-
.card .dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--text-muted)}
|
|
872
|
-
.card .dot.running{background:var(--ok);animation:dot-pulse 1.6s ease-in-out infinite}
|
|
873
|
-
.card .dot.idle{background:var(--text-muted)}
|
|
874
|
-
.card .dot.stopped{background:transparent;border:1px solid var(--text-faint)}
|
|
875
|
-
.card .dot.completed{background:var(--accent)}
|
|
876
|
-
.card .dot.error{background:var(--danger)}
|
|
936
|
+
.card .dot,.family-row .dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--text-muted)}
|
|
937
|
+
.card .dot.running,.family-row .dot.running{background:var(--ok);animation:dot-pulse 1.6s ease-in-out infinite}
|
|
938
|
+
.card .dot.idle,.family-row .dot.idle{background:var(--text-muted)}
|
|
939
|
+
.card .dot.stopped,.family-row .dot.stopped{background:transparent;border:1px solid var(--text-faint)}
|
|
940
|
+
.card .dot.completed,.family-row .dot.completed{background:var(--accent)}
|
|
941
|
+
.card .dot.error,.family-row .dot.error{background:var(--danger)}
|
|
877
942
|
@keyframes dot-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 55%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--ok) 0%,transparent)}}
|
|
878
|
-
.card .dot.waiting{background:var(--warn);animation:attn-pulse 1.6s ease-in-out infinite}
|
|
943
|
+
.card .dot.waiting,.family-row .dot.waiting{background:var(--warn);animation:attn-pulse 1.6s ease-in-out infinite}
|
|
879
944
|
@keyframes attn-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--warn) 60%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--warn) 0%,transparent)}}
|
|
880
|
-
@media (prefers-reduced-motion:reduce){.card .dot.running,.card .dot.waiting{animation:none}}
|
|
881
|
-
.card .attn{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn-bg);color:var(--warn-text);border:1px solid var(--warn-border)}
|
|
882
|
-
.card .stopped-badge{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn);color:var(--bg)}
|
|
945
|
+
@media (prefers-reduced-motion:reduce){.card .dot.running,.card .dot.waiting,.family-row .dot.running,.family-row .dot.waiting{animation:none}}
|
|
946
|
+
.card .attn,.family-row .attn{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn-bg);color:var(--warn-text);border:1px solid var(--warn-border)}
|
|
947
|
+
.card .stopped-badge,.family-row .stopped-badge{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn);color:var(--bg)}
|
|
883
948
|
.status.running{background:var(--ok-bg);color:var(--ok-text)}
|
|
884
949
|
.status.idle{background:var(--bg-raised)}
|
|
885
950
|
.status.completed{background:var(--accent-bg);color:var(--accent)}
|
|
886
951
|
.status.error{background:var(--danger-bg-dim);color:var(--danger-text)}
|
|
887
|
-
.card .owner{font-size:11px;padding:2px 6px;border-radius:999px}
|
|
888
|
-
.card .owner.worker{background:var(--ok-bg);color:var(--ok-text)}
|
|
889
|
-
.card .owner.tui{background:var(--warn-bg);color:var(--warn-text)}
|
|
952
|
+
.card .owner,.family-row .owner{font-size:11px;padding:2px 6px;border-radius:999px}
|
|
953
|
+
.card .owner.worker,.family-row .owner.worker{background:var(--ok-bg);color:var(--ok-text)}
|
|
954
|
+
.card .owner.tui,.family-row .owner.tui{background:var(--warn-bg);color:var(--warn-text)}
|
|
890
955
|
@media(max-width:900px){
|
|
891
956
|
#topStrip{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:72%;overflow-x:auto;min-height:0}
|
|
892
957
|
#topStrip #allTile{width:auto}
|
|
@@ -950,6 +1015,14 @@ a{color:var(--accent)}
|
|
|
950
1015
|
<input id="filter" type="search" placeholder="Search… (preview / id / status)" autocomplete="off">
|
|
951
1016
|
<label class="include-archived" title="Show archived sessions too"><input id="includeArchived" type="checkbox"> include archived</label>
|
|
952
1017
|
<span class="count" id="count"></span>
|
|
1018
|
+
<button class="ghost" id="selectBtn" title="Pick sessions to archive several at once">Select</button>
|
|
1019
|
+
<div id="selectBar" role="toolbar" aria-label="Selected sessions" hidden>
|
|
1020
|
+
<span id="selectCount" aria-live="polite"></span>
|
|
1021
|
+
<button class="ghost" id="selectAllBtn" title="Pick every session the search shows">Select all shown</button>
|
|
1022
|
+
<button id="batchArchiveBtn" title="Archive the picked sessions that are shown (their delegates too)">Archive 0</button>
|
|
1023
|
+
<button class="ghost" id="batchUnarchiveBtn" title="Unarchive the picked archived sessions that are shown" hidden>Unarchive 0</button>
|
|
1024
|
+
<button class="ghost" id="selectDoneBtn" title="Leave select mode (Esc)">Done</button>
|
|
1025
|
+
</div>
|
|
953
1026
|
</div>
|
|
954
1027
|
<div id="allPath" hidden>Stored in <code></code></div>
|
|
955
1028
|
<div id="allList"></div>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// The running turn's timing line ("turn 3 running · 4.1s", then "turn 3 ·
|
|
2
|
+
// 4.1s" at its end) and its one-second ticker, apart from app.js: the line's
|
|
3
|
+
// element, the turn id it is for, the number it shows, what the turn waits
|
|
4
|
+
// on (a provider retry, plugins' setup) and how many turns this page saw
|
|
5
|
+
// end. The DOM, the clock and the interval come in, so `node --test` drives
|
|
6
|
+
// it with a fake element.
|
|
7
|
+
import { elapsedSince, turnTimingText } from "./timing.js";
|
|
8
|
+
|
|
9
|
+
// The live line's text, or null when the turn's start is unknown. +inStage+:
|
|
10
|
+
// the stage's status row already says the turn runs, so the line drops its
|
|
11
|
+
// own "running" there.
|
|
12
|
+
export function liveLineText({ number, startedAt, note = "", inStage = false }, now = Date.now()) {
|
|
13
|
+
const duration = elapsedSince(startedAt, now);
|
|
14
|
+
if (duration === null) return null;
|
|
15
|
+
return turnTimingText(number, duration, { running: !inStage }) + (note ? ` · ${note}` : "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The record a turn-end read finishes its line with: the one with the
|
|
19
|
+
// turn's id; no id (an older worker's events): the last one.
|
|
20
|
+
export function lineRecord(records, turnId) {
|
|
21
|
+
return turnId ? records.find((r) => r.id === turnId) : records[records.length - 1];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// +timing+() the open session's timing (normalizeTiming's shape): its
|
|
25
|
+
// activeTurn is set at the start and cleared at the end;
|
|
26
|
+
// +place+(el) puts a new line in the page, true when that is the stage's
|
|
27
|
+
// status row (the line never moves mid-turn);
|
|
28
|
+
// +makeLine+() a new element for the line;
|
|
29
|
+
// +onTick+() after every tick and end (the info bar's session clock).
|
|
30
|
+
export function createLiveTiming({
|
|
31
|
+
timing,
|
|
32
|
+
place,
|
|
33
|
+
makeLine = () => document.createElement("div"),
|
|
34
|
+
onTick = () => {},
|
|
35
|
+
now = () => Date.now(),
|
|
36
|
+
every = (fn, ms) => setInterval(fn, ms),
|
|
37
|
+
cancel = (handle) => clearInterval(handle),
|
|
38
|
+
}) {
|
|
39
|
+
let el = null;
|
|
40
|
+
// The running turn's id (turn_started's turn_id, the id of its timing
|
|
41
|
+
// record), so a turn-end re-read finishes that turn's line with that
|
|
42
|
+
// turn's record, whatever started since.
|
|
43
|
+
let turnId = null;
|
|
44
|
+
let number = null;
|
|
45
|
+
// What the running turn waits on, after its live text; "" when it
|
|
46
|
+
// streams again.
|
|
47
|
+
let note = "";
|
|
48
|
+
let inStage = false;
|
|
49
|
+
let ticker = null;
|
|
50
|
+
// The turns this page saw end: the records of its last full render, +1 at
|
|
51
|
+
// each turn's end, never fewer than a merge brought. The live line numbers
|
|
52
|
+
// the next one, so a turn queued behind another reads its own number while
|
|
53
|
+
// the other's re-read is still out.
|
|
54
|
+
let ended = 0;
|
|
55
|
+
|
|
56
|
+
function stopTicker() {
|
|
57
|
+
if (ticker !== null) {
|
|
58
|
+
cancel(ticker);
|
|
59
|
+
ticker = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function tick() {
|
|
64
|
+
const text = liveLineText({ number, startedAt: timing().activeTurn?.started_at, note, inStage }, now());
|
|
65
|
+
if (el && text !== null) el.textContent = text;
|
|
66
|
+
onTick();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
get el() { return el; },
|
|
71
|
+
get turnId() { return turnId; },
|
|
72
|
+
// The replayed turn_started of a re-render carries no id: the running
|
|
73
|
+
// turn's record's.
|
|
74
|
+
set turnId(id) { turnId = id; },
|
|
75
|
+
get ended() { return ended; },
|
|
76
|
+
set ended(n) { ended = n; },
|
|
77
|
+
get ticking() { return ticker !== null; },
|
|
78
|
+
|
|
79
|
+
start(startedAt = new Date(now()).toISOString(), id = null) {
|
|
80
|
+
stopTicker();
|
|
81
|
+
timing().activeTurn = id ? { id, started_at: startedAt } : { started_at: startedAt };
|
|
82
|
+
turnId = id || null;
|
|
83
|
+
// The turns ended so far, so this one comes next.
|
|
84
|
+
number = ended + 1;
|
|
85
|
+
note = "";
|
|
86
|
+
el = makeLine();
|
|
87
|
+
el.className = "turn-timing live";
|
|
88
|
+
inStage = !!place(el);
|
|
89
|
+
tick();
|
|
90
|
+
ticker = every(tick, 1000);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
tick,
|
|
94
|
+
|
|
95
|
+
// Set (or clear, "") what the running turn waits on.
|
|
96
|
+
setNote(text) {
|
|
97
|
+
if (note === text) return;
|
|
98
|
+
note = text;
|
|
99
|
+
if (timing().activeTurn) tick();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// The turn ended: the line stops with its elapsed time (a canceled turn's
|
|
103
|
+
// says so, as a reload's does), numbered as it ran until a re-read
|
|
104
|
+
// (finishLine) has the record.
|
|
105
|
+
finish({ canceled = false } = {}) {
|
|
106
|
+
const duration = elapsedSince(timing().activeTurn?.started_at, now());
|
|
107
|
+
if (el && duration !== null) {
|
|
108
|
+
note = "";
|
|
109
|
+
el.classList.remove("live");
|
|
110
|
+
el.textContent = turnTimingText(number, duration, { canceled });
|
|
111
|
+
}
|
|
112
|
+
timing().activeTurn = null;
|
|
113
|
+
stopTicker();
|
|
114
|
+
onTick();
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// The line and the turn a turn-end read is for, taken before its await:
|
|
118
|
+
// a turn queued behind this one may start (a new live line) meanwhile.
|
|
119
|
+
capture() { return { el, turnId }; },
|
|
120
|
+
|
|
121
|
+
// Finish the line a turn-end read captured (+line+: {el, turnId}) with
|
|
122
|
+
// that turn's record: its place in the records and its duration (a
|
|
123
|
+
// canceled one says so). No record: the line stays as the turn's end
|
|
124
|
+
// left it. The ticker stops only if that line is still the live one.
|
|
125
|
+
finishLine(line) {
|
|
126
|
+
const records = timing().turnRecords;
|
|
127
|
+
const record = lineRecord(records, line.turnId);
|
|
128
|
+
if (line.el === el) {
|
|
129
|
+
timing().activeTurn = null;
|
|
130
|
+
stopTicker();
|
|
131
|
+
}
|
|
132
|
+
const duration = Number(record?.duration_ms);
|
|
133
|
+
if (line.el && record && Number.isFinite(duration)) {
|
|
134
|
+
line.el.classList.remove("live");
|
|
135
|
+
line.el.textContent = turnTimingText(records.indexOf(record) + 1, duration, { canceled: record.status === "canceled" });
|
|
136
|
+
}
|
|
137
|
+
onTick();
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// A new turn starts: the old line (finished or not) is no longer the
|
|
141
|
+
// live one.
|
|
142
|
+
detach() {
|
|
143
|
+
stopTicker();
|
|
144
|
+
el = null;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// The session (or its rendering) starts over.
|
|
148
|
+
drop() {
|
|
149
|
+
stopTicker();
|
|
150
|
+
el = null;
|
|
151
|
+
turnId = null;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|