samagotchi 0.134.0 → 0.135.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 +49 -1
- data/bin/chi +2 -3
- data/docs/architecture.md +6 -3
- data/docs/cli.md +39 -34
- data/docs/configuration.md +1 -2
- data/docs/guardrails.md +53 -9
- data/docs/internals/engine.md +6 -3
- data/docs/internals/participants.md +1 -1
- data/docs/internals/web-frontend.md +262 -0
- data/docs/sessions.md +2 -0
- data/docs/testing.md +27 -2
- data/lib/samagotchi/bridge.rb +11 -6
- data/lib/samagotchi/bridge_client.rb +8 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +0 -2
- data/lib/samagotchi/engine.rb +69 -12
- data/lib/samagotchi/guardrail_wiring.rb +8 -6
- data/lib/samagotchi/guardrails/approval.rb +41 -5
- data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
- data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
- data/lib/samagotchi/guardrails/pair_command.rb +387 -78
- data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
- data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
- data/lib/samagotchi/guardrails/shared_git.rb +1 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
- data/lib/samagotchi/guardrails_command.rb +13 -10
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/message_parts.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +33 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager.rb +13 -11
- data/lib/samagotchi/system_one/approval_question.rb +21 -16
- data/lib/samagotchi/system_one/self_protect.rb +13 -15
- data/lib/samagotchi/tools/delegate_relay.rb +6 -5
- data/lib/samagotchi/tools/delegate_wait.rb +5 -5
- data/lib/samagotchi/tools/execute.rb +6 -6
- data/lib/samagotchi/tools/output_guardrails.rb +9 -2
- data/lib/samagotchi/tools/read.rb +5 -5
- data/lib/samagotchi/turn_anchors.rb +49 -0
- data/lib/samagotchi/verify_anchor.rb +5 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -14
- data/lib/samagotchi/web/message_display.rb +17 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_view.rb +8 -0
- data/lib/samagotchi/web/public/activity.js +1 -1
- data/lib/samagotchi/web/public/annotations.js +1 -1
- data/lib/samagotchi/web/public/answer_fold.js +3 -3
- data/lib/samagotchi/web/public/app.js +447 -340
- data/lib/samagotchi/web/public/bubbles.js +3 -3
- data/lib/samagotchi/web/public/cards_place.js +4 -2
- data/lib/samagotchi/web/public/composer.css +8 -0
- data/lib/samagotchi/web/public/context_chips.js +9 -6
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/dock_reserve.js +13 -15
- data/lib/samagotchi/web/public/feature_gates.js +33 -0
- data/lib/samagotchi/web/public/history_reveal.js +8 -4
- data/lib/samagotchi/web/public/index.html +38 -9
- data/lib/samagotchi/web/public/latest.js +46 -0
- data/lib/samagotchi/web/public/lists_view.js +15 -13
- data/lib/samagotchi/web/public/live_timing.js +2 -0
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/method_api.js +14 -0
- data/lib/samagotchi/web/public/notifications.js +4 -11
- data/lib/samagotchi/web/public/participant.js +11 -6
- data/lib/samagotchi/web/public/participant_names.js +1 -1
- data/lib/samagotchi/web/public/participants_panel.js +4 -2
- data/lib/samagotchi/web/public/project_picker.js +18 -22
- data/lib/samagotchi/web/public/question_cards.js +2 -2
- data/lib/samagotchi/web/public/restore.js +2 -0
- data/lib/samagotchi/web/public/revise_stack.js +372 -0
- data/lib/samagotchi/web/public/scope.js +12 -20
- data/lib/samagotchi/web/public/send_turn.js +29 -16
- data/lib/samagotchi/web/public/session_actions.js +15 -14
- data/lib/samagotchi/web/public/session_fields.js +152 -0
- data/lib/samagotchi/web/public/session_payload.js +35 -0
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/session_store.js +191 -0
- data/lib/samagotchi/web/public/stage_model.js +4 -4
- data/lib/samagotchi/web/public/stage_view.js +11 -16
- data/lib/samagotchi/web/public/stream_handlers.js +88 -90
- data/lib/samagotchi/web/public/task_stop.js +1 -1
- data/lib/samagotchi/web/public/timing.js +2 -2
- data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +27 -92
- data/lib/samagotchi/web/public/turn_events.js +2 -2
- data/lib/samagotchi/web/public/turn_html.js +5 -3
- data/lib/samagotchi/web/public/turn_model.js +6 -6
- data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +3 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/web/session_control.rb +2 -2
- data/lib/samagotchi/worker.rb +9 -9
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +12 -3
- data/lib/samagotchi/web/public/hold.js +0 -78
|
@@ -105,7 +105,7 @@ export function setBubbleDelivery(bubble, delivery) {
|
|
|
105
105
|
// @param appendToHistory (el) puts +el+ at the history's end (or the stage's,
|
|
106
106
|
// while it holds a turn), above the live timing line
|
|
107
107
|
// @param isNearBottom () => whether the history follows its end
|
|
108
|
-
// @param turnDom the running turn's
|
|
108
|
+
// @param turnDom the running turn's DOM (stage_view.js)
|
|
109
109
|
// @param findInTurnRoots (selector) => the matching elements in the history
|
|
110
110
|
// and the stage
|
|
111
111
|
// @param shownTexts the running turn's texts on the page (shown_texts.js)
|
|
@@ -149,7 +149,7 @@ export function createBubbles({
|
|
|
149
149
|
|
|
150
150
|
function addEndBubble(text, { stopped = false } = {}) {
|
|
151
151
|
appendToHistory(elementOf(endBubbleTemplate(text, { stopped })));
|
|
152
|
-
if (!turnDom.holds
|
|
152
|
+
if (!turnDom.holds()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// ── Prompt bubbles ───────────────────────────────────────────────────────
|
|
@@ -239,7 +239,7 @@ export function createBubbles({
|
|
|
239
239
|
const details = elementOf(recapTemplate(text, turnsSince, createdAt));
|
|
240
240
|
// A stale one sits where it was written, above the turns it doesn't cover.
|
|
241
241
|
// Never in the stage: a recap is between turns.
|
|
242
|
-
turnDom.settleFold
|
|
242
|
+
turnDom.settleFold();
|
|
243
243
|
const prompt = recapPlace(turnPromptBubbles(historyEl), turnsSince);
|
|
244
244
|
// Above that turn's gap divider too: the recap was written before the quiet stretch ended.
|
|
245
245
|
const before = prompt?.previousElementSibling?.matches(`.${TIME_GAP_CLASS}`) ? prompt.previousElementSibling : prompt;
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
import { getSession, sendCommand } from "./data.js";
|
|
11
11
|
import { cardPlace, drawCard, isCard, isNoticeCard, refreshCardBody, turnNoticePlace } from "./card.js";
|
|
12
12
|
import { elementOf } from "./turn_html.js";
|
|
13
|
-
import { stepText } from "./
|
|
13
|
+
import { stepText } from "./turn_block.js";
|
|
14
14
|
import { llmContextNotice, noticeLine } from "./turn_events.js";
|
|
15
15
|
import { hookNoticeTemplate, TURN_PROMPT, turnPromptBubbles } from "./bubbles.js";
|
|
16
|
+
import { stillOn } from "./latest.js";
|
|
16
17
|
|
|
17
18
|
// The elements of the reloaded turn that ends before +before+ (the next
|
|
18
19
|
// turn's prompt, or null for the last turn), its prompt first.
|
|
@@ -145,10 +146,11 @@ export function createCardsPlace({
|
|
|
145
146
|
function refreshCardBodies() {
|
|
146
147
|
clearTimeout(cardBodiesTimer);
|
|
147
148
|
const id = selected();
|
|
149
|
+
const still = stillOn(selected, id);
|
|
148
150
|
cardBodiesTimer = setTimeout(async () => {
|
|
149
151
|
try {
|
|
150
152
|
const data = await api.getSession(id, { cards: true });
|
|
151
|
-
if (
|
|
153
|
+
if (!still()) return;
|
|
152
154
|
for (const card of data.cards || []) {
|
|
153
155
|
const el = card.type === "card" && cardElement(card.id);
|
|
154
156
|
if (el) refreshCardBody(el, card);
|
|
@@ -24,6 +24,14 @@ body.no-glass .composer-card{background:var(--bg-panel);-webkit-backdrop-filter:
|
|
|
24
24
|
body.has-session #composer{flex-wrap:wrap;row-gap:6px}
|
|
25
25
|
#chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
26
26
|
#chips[hidden]{display:none}
|
|
27
|
+
/* The armed Revise (revise_stack.js reviseChipTemplate): "Revising: ‹first line› ✕". */
|
|
28
|
+
#reviseChip{display:flex;min-width:0}
|
|
29
|
+
#reviseChip[hidden]{display:none}
|
|
30
|
+
.revise-chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;padding:2px 4px 2px 10px;border-radius:999px;border:1px solid var(--accent-border);background:var(--accent-bg);color:var(--text-2);font-size:12px;line-height:18px}
|
|
31
|
+
.revise-chip.refused{border-color:var(--warn-border);background:var(--warn-bg);color:var(--warn-text)}
|
|
32
|
+
.revise-chip-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
33
|
+
.revise-chip-clear{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;min-height:0;padding:0;border:0;border-radius:999px;background:transparent;color:var(--text-muted);font-size:11px;cursor:pointer}
|
|
34
|
+
.revise-chip-clear:hover{background:var(--bg-raised);color:var(--text-bright);filter:none}
|
|
27
35
|
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--accent-bg);border:1px solid var(--border-strong);border-radius:8px;padding:3px 4px 3px 3px;font-size:12px;max-width:220px}
|
|
28
36
|
.chip img{width:32px;height:32px;object-fit:cover;border-radius:5px}
|
|
29
37
|
.chip .chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -11,6 +11,7 @@ import { html, nothing, render } from "./vendor/lit-html.js";
|
|
|
11
11
|
import { keepFocus } from "./keep_focus.js";
|
|
12
12
|
import { popoverPlace } from "./chip_popover.js";
|
|
13
13
|
import { cantText } from "./format.js";
|
|
14
|
+
import { createLatest, stillOn } from "./latest.js";
|
|
14
15
|
|
|
15
16
|
// "now", "3m", "2h", "4d" since an ISO time; "" for none.
|
|
16
17
|
export function ageText(iso, now = Date.now()) {
|
|
@@ -106,7 +107,8 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
106
107
|
let rows = [];
|
|
107
108
|
let canAdd = false;
|
|
108
109
|
let openName = null;
|
|
109
|
-
|
|
110
|
+
// Only the newest list read for the open session is drawn (latest.js).
|
|
111
|
+
const latest = createLatest(() => sessionId);
|
|
110
112
|
// The open row's text once read (view text), and the "+ URL" form's
|
|
111
113
|
// state ({error, busy}).
|
|
112
114
|
let text = null;
|
|
@@ -140,15 +142,15 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
140
142
|
async function refresh() {
|
|
141
143
|
const id = sessionId;
|
|
142
144
|
if (!id) return;
|
|
143
|
-
const
|
|
145
|
+
const fresh = latest.begin();
|
|
144
146
|
try {
|
|
145
147
|
const data = await api.list(id);
|
|
146
|
-
if (
|
|
148
|
+
if (!fresh()) return;
|
|
147
149
|
rows = data.sources || [];
|
|
148
150
|
canAdd = data.can_add_url === true;
|
|
149
151
|
} catch {
|
|
150
152
|
// A session the server doesn't know (gone, or an older chi web): no chips.
|
|
151
|
-
if (
|
|
153
|
+
if (!fresh()) return;
|
|
152
154
|
rows = [];
|
|
153
155
|
canAdd = false;
|
|
154
156
|
}
|
|
@@ -234,15 +236,16 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
234
236
|
const url = form.elements.url.value.trim();
|
|
235
237
|
const why = form.elements.why.value.trim();
|
|
236
238
|
if (!id || !url) return;
|
|
239
|
+
const still = stillOn(() => sessionId);
|
|
237
240
|
drawAdd({ busy: true });
|
|
238
241
|
try {
|
|
239
242
|
const data = await api.add(id, { url, why: why || undefined });
|
|
240
|
-
if (
|
|
243
|
+
if (!still()) return;
|
|
241
244
|
close();
|
|
242
245
|
await refresh();
|
|
243
246
|
toast(`Attached ${data.name}: the agent gets a note before your next message`);
|
|
244
247
|
} catch (e) {
|
|
245
|
-
if (
|
|
248
|
+
if (!still() || openName !== ADD) return;
|
|
246
249
|
// The same form again: what the user typed stays; the URL gets focus.
|
|
247
250
|
drawAdd({ busy: false, error: e.message });
|
|
248
251
|
pop.querySelector('input[name="url"]')?.focus();
|
|
@@ -130,7 +130,7 @@ export function uploadImage(id, file, name, opts = {}) {
|
|
|
130
130
|
// (mergeTiming), and opts.turnId asks for that turn's answer.
|
|
131
131
|
// opts.timing: the whole timing alone (when the merge came up short).
|
|
132
132
|
// opts.parts: each saved message with what it did (thinking, tool calls
|
|
133
|
-
// and their output), for the turn
|
|
133
|
+
// and their output), for the turn block's reload.
|
|
134
134
|
export function getSession(id, { tail, recent, turnId, timing, parts, cards, ...opts } = {}) {
|
|
135
135
|
let query = cards ? "?cards=1" : tail ? "?tail=1" : timing ? "?timing=1" : parts ? "?parts=1" : "";
|
|
136
136
|
if (tail && recent) query += "&recent=1";
|
|
@@ -144,12 +144,14 @@ export function getSession(id, { tail, recent, turnId, timing, parts, cards, ...
|
|
|
144
144
|
// image-only send, whose text is only its [image: …] placeholders).
|
|
145
145
|
// delivery: what the message does to a running turn ("cut": /cut TEXT);
|
|
146
146
|
// none = the server's default (the next step).
|
|
147
|
-
export function sendTurn(id, prompt, { clientId, images, history = true, delivery = null, verify = null, ...opts } = {}) {
|
|
147
|
+
export function sendTurn(id, prompt, { clientId, images, history = true, delivery = null, verify = null, revises = null, ...opts } = {}) {
|
|
148
148
|
const body = clientId ? { prompt, client_id: clientId } : { prompt };
|
|
149
149
|
if (images?.length) body.images = images;
|
|
150
150
|
if (delivery) body.delivery = delivery;
|
|
151
151
|
if (history === false) body.history = false;
|
|
152
152
|
if (verify) body.verify = verify;
|
|
153
|
+
// A Revise turn's draft: {turn_id} of the answer it rewrites.
|
|
154
|
+
if (revises) body.revises = revises;
|
|
153
155
|
return api(`/api/sessions/${id}/turn`, {
|
|
154
156
|
method: "POST",
|
|
155
157
|
body: JSON.stringify(body),
|
|
@@ -21,7 +21,7 @@ import { toastBottom } from "./toast_place.js";
|
|
|
21
21
|
// model panel (toast_place.js); with no card in sight (the start page, all
|
|
22
22
|
// sessions) the CSS's 24 px from the bottom (--toast-bottom unset).
|
|
23
23
|
//
|
|
24
|
-
// +stageEl+: the stage (
|
|
24
|
+
// +stageEl+: the stage (stage_view.js). +atHistoryEnd()+:
|
|
25
25
|
// the reader is at the history's end (app.js, scroll.js's threshold).
|
|
26
26
|
// +reduce()+ / +ease+: prefers-reduced-motion and the eased follow
|
|
27
27
|
// (scroll.js), defaults the page's.
|
|
@@ -37,7 +37,7 @@ export function createDockReserve({
|
|
|
37
37
|
const win = doc.defaultView;
|
|
38
38
|
|
|
39
39
|
function dockReserve() {
|
|
40
|
-
const shown =
|
|
40
|
+
const shown = !stageEl.hidden && !stageEl.classList.contains("leaving");
|
|
41
41
|
return dockEl.offsetHeight + (shown ? stageEl.offsetHeight + STAGE_GAP : 0);
|
|
42
42
|
}
|
|
43
43
|
const historyEnd = () => historyEl.scrollHeight - historyEl.clientHeight;
|
|
@@ -48,7 +48,7 @@ export function createDockReserve({
|
|
|
48
48
|
// counts as at the end.
|
|
49
49
|
function updateReserve({ fromStage = false } = {}) {
|
|
50
50
|
const follow = atHistoryEnd() || historyEl.dataset.following !== undefined;
|
|
51
|
-
const leaving =
|
|
51
|
+
const leaving = stageEl.classList.contains("leaving");
|
|
52
52
|
centerEl.classList.toggle("reserve-easing", leaving);
|
|
53
53
|
const reserve = `${dockReserve()}px`;
|
|
54
54
|
const changed = centerEl.style.getPropertyValue("--dock-h") !== reserve;
|
|
@@ -83,18 +83,16 @@ export function createDockReserve({
|
|
|
83
83
|
|
|
84
84
|
const dockObserver = new win.ResizeObserver((entries) => updateReserve({ fromStage: entries.some((e) => e.target === stageEl) }));
|
|
85
85
|
dockObserver.observe(dockEl);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}).observe(stageEl, { attributes: true, attributeFilter: ["class", "hidden"] });
|
|
97
|
-
}
|
|
86
|
+
dockObserver.observe(stageEl);
|
|
87
|
+
// Only showing, hiding and leaving change the reserve (not the stage's
|
|
88
|
+
// other classes: waiting, in-use…).
|
|
89
|
+
let stageShown = "";
|
|
90
|
+
new win.MutationObserver(() => {
|
|
91
|
+
const shown = `${stageEl.hidden}|${stageEl.classList.contains("leaving")}`;
|
|
92
|
+
if (shown === stageShown) return;
|
|
93
|
+
stageShown = shown;
|
|
94
|
+
updateReserve({ fromStage: true });
|
|
95
|
+
}).observe(stageEl, { attributes: true, attributeFilter: ["class", "hidden"] });
|
|
98
96
|
win.addEventListener("resize", () => placeToast());
|
|
99
97
|
// The model panel opening, closing or changing height (its list filters).
|
|
100
98
|
new win.ResizeObserver(() => placeToast()).observe(modelPanelEl);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The open session's feature gates: a page action that its worker must
|
|
2
|
+
// support (its sidecar features, a live bridge), and the redraw that
|
|
3
|
+
// follows when that flips. Registered with the session store as its
|
|
4
|
+
// `featureGates` redraw (session_store.js ORDER): it runs whenever a field
|
|
5
|
+
// it reads changes, and a gate's own redraw only when its answer flipped.
|
|
6
|
+
//
|
|
7
|
+
// A session switch (forgetSession) and a full load (renderSessionView) draw
|
|
8
|
+
// every card with the gate's answer already: they prime() the gates, so the
|
|
9
|
+
// redraw that follows their write sees no flip. A write anywhere else (a
|
|
10
|
+
// restart's hub event bringing the feature, the worker going) fires it.
|
|
11
|
+
//
|
|
12
|
+
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
13
|
+
// new export of an old module would leave a cached copy without it.
|
|
14
|
+
|
|
15
|
+
// @param entries [{name, unavailable(current) → bool, redraw()}]
|
|
16
|
+
// @return {prime(current), check(current)}: prime takes each gate's answer
|
|
17
|
+
// without redrawing; check redraws the gates whose answer flipped since
|
|
18
|
+
export function createFeatureGates(entries) {
|
|
19
|
+
const last = new Map();
|
|
20
|
+
return {
|
|
21
|
+
prime(current) {
|
|
22
|
+
for (const gate of entries) last.set(gate.name, !!gate.unavailable(current));
|
|
23
|
+
},
|
|
24
|
+
check(current) {
|
|
25
|
+
for (const gate of entries) {
|
|
26
|
+
const now = !!gate.unavailable(current);
|
|
27
|
+
const flipped = last.has(gate.name) && last.get(gate.name) !== now;
|
|
28
|
+
last.set(gate.name, now);
|
|
29
|
+
if (flipped) gate.redraw();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
// The one way a jump into the history shows what it lands on: whatever
|
|
2
2
|
// hides +el+ unfolds: a closed <details> around it (a turn's steps block, a
|
|
3
3
|
// step, a diff), an answer folded by hand (answer_fold.js: its row follows
|
|
4
|
-
// the bubble's class; the jump's unfold isn't saved)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// the bubble's class; the jump's unfold isn't saved), a superseded version
|
|
5
|
+
// of a Revise stack (revise_stack.js: its stub opens, not saved). Every
|
|
6
|
+
// jump goes through it (claim_link.js showClaim, refs.js showRow and so the
|
|
7
|
+
// working set's step tags, verdict_card.js "View check"), so a new way to
|
|
8
|
+
// hide history is opened in one place.
|
|
8
9
|
//
|
|
9
10
|
// Pure DOM, so it runs under `node --test` with happy-dom. A module of its
|
|
10
11
|
// own: static JS is cached at unversioned URLs, and a new export on an old
|
|
11
12
|
// module would blank the page.
|
|
12
13
|
|
|
14
|
+
import { openStubOver } from "./revise_stack.js";
|
|
15
|
+
|
|
13
16
|
// Opens each closed <details> that holds +el+ (not +el+ itself: a fold the
|
|
14
17
|
// caller means to open it opens itself) and unfolds the folded answer that
|
|
15
18
|
// is or holds it. +el+, or null.
|
|
16
19
|
export function revealInHistory(el) {
|
|
17
20
|
if (!el) return null;
|
|
21
|
+
openStubOver(el);
|
|
18
22
|
el.closest?.(".bubble.output.folded")?.classList.remove("folded");
|
|
19
23
|
for (let details = el.parentElement?.closest("details"); details; details = details.parentElement?.closest("details")) {
|
|
20
24
|
details.open = true;
|
|
@@ -57,9 +57,6 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
57
57
|
/* A generation opens its answer bubble empty: hidden until text streams (the thinking box shows progress meanwhile). */
|
|
58
58
|
.bubble.output.streaming:empty{display:none}
|
|
59
59
|
/* The answer's pop: the promoted bubble is held at the narration box's height (inline max-height, scrolled to its tail) until the rendered markdown lands, then grows to its content while the content fades in; both classes go 400 ms later. */
|
|
60
|
-
.bubble.output.boxed{overflow:hidden;transition:max-height 360ms ease}
|
|
61
|
-
.bubble.output.revealing>:not(.copy-btn){animation:fadeIn 240ms ease}
|
|
62
|
-
@media (prefers-reduced-motion:reduce){.bubble.output.boxed{transition:none}.bubble.output.revealing>:not(.copy-btn){animation:none}}
|
|
63
60
|
/* A long answer folded by hand (answer_fold.js): the row is the bubble's sibling just before it, styled as a closed turn's steps; folded, the bubble is hidden and the row reads its first line and size; unfolded, a slim "▾ N lines" header close above it. */
|
|
64
61
|
.bubble.output.folded{display:none}
|
|
65
62
|
.answer-fold{display:flex;align-items:baseline;gap:0;align-self:flex-start;max-width:100%;min-width:0;margin:0;padding:3px 10px;font:inherit;font-size:12px;font-weight:500;line-height:1.4;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:transparent;border:1px solid transparent;border-left:3px solid var(--accent-strong);border-radius:6px;transition:background .12s ease}
|
|
@@ -71,6 +68,37 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
71
68
|
.answer-fold-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);font-size:13px}
|
|
72
69
|
.answer-fold-label:empty{display:none}
|
|
73
70
|
.answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
71
|
+
/* Revise stacks (revise_stack.js): a superseded version folds to its stub; the Revise row under a final answer
|
|
72
|
+
(zero height: its button sits on the bubble's bottom edge, clear of the copy button, shown on hover) and a stack
|
|
73
|
+
top's version chip. */
|
|
74
|
+
.stack-hidden,.time-gap.stack-gap{display:none!important}
|
|
75
|
+
.stack-stub{display:flex;align-items:baseline;gap:8px;align-self:stretch;max-width:100%;min-width:0;margin:0;padding:4px 10px;font:inherit;font-size:12px;line-height:1.4;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:transparent;border:1px dashed var(--border-strong);border-radius:8px;transition:background .12s ease}
|
|
76
|
+
.stack-stub:hover{background:var(--bg-panel)}
|
|
77
|
+
.stack-stub:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
78
|
+
.stack-stub::before{content:"▸";color:var(--accent);flex:none}
|
|
79
|
+
.stack-stub.open::before{content:"▾"}
|
|
80
|
+
.stack-stub.open{border-style:solid;border-color:transparent;border-left:3px solid var(--border-strong);border-radius:6px}
|
|
81
|
+
.stack-stub-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
82
|
+
.stack-stub-gap{flex:none;margin-left:auto;color:var(--time-gap);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
83
|
+
.revise-row{position:relative;height:0;margin-top:-12px;display:flex;justify-content:flex-end;align-items:center;gap:8px}
|
|
84
|
+
.revise-row.stacked{height:auto;margin-top:-6px;justify-content:flex-start}
|
|
85
|
+
.revise-btn{min-height:0;height:22px;padding:0 8px;border-radius:6px;font-size:11px;line-height:20px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-muted);cursor:pointer;opacity:0;transition:opacity .12s ease,color .12s ease}
|
|
86
|
+
.revise-row:not(.stacked) .revise-btn{position:absolute;right:14px;top:-10px;z-index:1;height:20px;line-height:18px}
|
|
87
|
+
.revise-btn:hover:not(:disabled){color:var(--text-bright);filter:none}
|
|
88
|
+
.revise-btn:disabled{cursor:default}
|
|
89
|
+
.revise-btn.armed,.revise-btn.armed:hover:not(:disabled){opacity:1;color:var(--on-solid);background:var(--accent-strong);border-color:var(--accent-strong)}
|
|
90
|
+
.bubble.output:hover+.revise-row .revise-btn,.revise-row:hover .revise-btn,.revise-row.stacked .revise-btn,.revise-btn:focus-visible{opacity:1}
|
|
91
|
+
@media (hover:none){.revise-btn{opacity:.55}}
|
|
92
|
+
.stack-versions{position:relative;font-size:12px;color:var(--text-muted)}
|
|
93
|
+
.stack-versions>summary{list-style:none;cursor:pointer;padding:1px 8px;border-radius:999px;border:1px solid var(--accent-border);background:var(--accent-bg);color:var(--text-2);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
94
|
+
.stack-versions>summary::-webkit-details-marker{display:none}
|
|
95
|
+
.stack-versions>summary::after{content:"\00a0▾";color:var(--accent)}
|
|
96
|
+
.stack-versions[open]>summary::after{content:"\00a0▴"}
|
|
97
|
+
.stack-versions-list{position:absolute;z-index:5;top:calc(100% + 4px);left:0;display:flex;flex-direction:column;min-width:240px;max-width:min(520px,calc(100vw - 32px));padding:4px;border-radius:8px;border:1px solid var(--border-strong);background:var(--bg-raised);box-shadow:var(--toast-shadow)}
|
|
98
|
+
.stack-version{display:block;min-height:0;padding:4px 8px;border:0;border-radius:6px;background:transparent;color:var(--text-2);font:inherit;font-size:12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
99
|
+
.stack-version:hover:not(:disabled){background:var(--bg-hover);filter:none}
|
|
100
|
+
.stack-version.current{color:var(--text-muted);cursor:default}
|
|
101
|
+
.stack-version-n{font-weight:600;color:var(--accent);font-variant-numeric:tabular-nums}
|
|
74
102
|
/* A gap divider (time_gaps.js): one centred dim line where a quiet stretch passed, never wrapped (a phone at 360 px keeps it on one line). Not a .bubble. */
|
|
75
103
|
.time-gap{align-self:stretch;text-align:center;color:var(--time-gap);font-size:12px;line-height:1.4;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:2px 0;user-select:none}
|
|
76
104
|
.turn-timing{margin-top:8px;color:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}
|
|
@@ -286,7 +314,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
|
|
|
286
314
|
.bubble.thinking>summary::before{content:"▸ ";color:var(--think)}
|
|
287
315
|
.bubble.thinking[open]>summary::before{content:"▾ "}
|
|
288
316
|
.thinking-body{margin-top:6px;font-size:13px;color:var(--text-3);white-space:pre-wrap;word-break:break-word;font-style:italic;max-height:240px;overflow:auto}
|
|
289
|
-
/*
|
|
317
|
+
/* The turn block (the stage's cloud, the history): one block per turn, its steps as collapsed rows, the live one open on top. */
|
|
290
318
|
.turn-work{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;background:var(--bg-panel);padding:8px 12px;font-size:14px}
|
|
291
319
|
.turn-work>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
|
|
292
320
|
.turn-work>summary::-webkit-details-marker{display:none}
|
|
@@ -303,7 +331,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
|
|
|
303
331
|
.gen>summary::-webkit-details-marker{display:none}
|
|
304
332
|
.gen>summary::before{content:"▸ ";color:var(--text-faint)}
|
|
305
333
|
.gen[open]>summary::before{content:"▾ "}
|
|
306
|
-
/* Open, a step whose label starts with its body's first words (
|
|
334
|
+
/* Open, a step whose label starts with its body's first words (turn_block.js labelParts) says only its calls. */
|
|
307
335
|
.gen[open]>summary.repeats>:is(.gen-head,.gen-sep){display:none}
|
|
308
336
|
/* A step with a failed call (status error): its chevron in the danger colour. */
|
|
309
337
|
.gen.has-failed>summary::before{color:var(--danger)}
|
|
@@ -327,7 +355,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
|
|
|
327
355
|
.gen details.thinking summary.thinking-fade{animation:thinking-fade 120ms ease-out}
|
|
328
356
|
@keyframes thinking-fade{from{opacity:.4}to{opacity:1}}
|
|
329
357
|
@media (prefers-reduced-motion:reduce){.gen details.thinking summary.thinking-fade{animation:none}}
|
|
330
|
-
/* A finished block's outline (
|
|
358
|
+
/* A finished block's outline (turn_block.js outlineBlock): open on its narrated steps' rows; a run of steps that said
|
|
331
359
|
nothing is one "· N steps" fold, a row like a step's, that shows them. */
|
|
332
360
|
.turn-work.outline{--step-indent:14px}
|
|
333
361
|
.turn-work.outline>.gen.folded:not(.unfolded){display:none}
|
|
@@ -676,9 +704,9 @@ body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){dis
|
|
|
676
704
|
#commandList .command-desc{flex:1;min-width:0;font-size:13px;color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
677
705
|
#commandList .command-note{font-size:11px;color:var(--text-faint);white-space:nowrap}
|
|
678
706
|
@media(max-width:600px){#commandList{left:6px;width:calc(100% - 12px)}#commandList .command-option{flex-wrap:wrap;row-gap:0;padding:8px 10px}#commandList .command-desc{flex-basis:100%;order:3}}
|
|
679
|
-
/* The stage
|
|
707
|
+
/* The stage (stage_view.js): the running turn as the dock card's top section, pinned above the composer.
|
|
680
708
|
Fixed slots (status row, prompt line, a two-line headline, the tool row, a one-line trail) never change height while the
|
|
681
|
-
turn runs; cards and rows that arrive meanwhile go in their own capped scroller (extras); the cloud is the turn
|
|
709
|
+
turn runs; cards and rows that arrive meanwhile go in their own capped scroller (extras); the cloud is the turn
|
|
682
710
|
block behind a chip of ticks. --stage-max (stage_view.js fitCap) keeps it inside the session panel. */
|
|
683
711
|
/* D2: its own glass card above the composer card (as a section of one card the composer's focus ring wrapped the turn too):
|
|
684
712
|
in the stage view #dock is a clear column holding the stage and .dock-card (the composer and its footer, stage_view.js). */
|
|
@@ -791,7 +819,7 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
791
819
|
.ts-answer{display:flex;flex-direction:column;gap:8px}
|
|
792
820
|
.ts-answer>.bubble.output{border-left:0;background:transparent;border:0;padding:0}
|
|
793
821
|
.ts-answer>.bubble.output>.copy-btn{top:0;right:0}
|
|
794
|
-
/* The cloud: one chip (steps, calls, a tick per call), the turn
|
|
822
|
+
/* The cloud: one chip (steps, calls, a tick per call), the turn block behind it. */
|
|
795
823
|
/* The footer: the chip at the stage's bottom edge (the cloud's steps open above it, the scroller's last slot). */
|
|
796
824
|
.ts-foot{position:relative;flex:none;padding:0 14px 10px}
|
|
797
825
|
/* The question room's jump pill (stage_view.js drawPill): at the scroller's bottom edge, above the chip, while the waiting
|
|
@@ -1654,6 +1682,7 @@ a{color:var(--accent)}
|
|
|
1654
1682
|
</div>
|
|
1655
1683
|
<div id="deliveryLine" class="composer-line" aria-live="off" hidden></div>
|
|
1656
1684
|
<div class="compose-col">
|
|
1685
|
+
<div id="reviseChip" hidden></div>
|
|
1657
1686
|
<div id="chips" hidden></div>
|
|
1658
1687
|
<textarea id="prompt" placeholder="Ask chi anything…" aria-controls="commandList" aria-autocomplete="list" aria-expanded="false"></textarea>
|
|
1659
1688
|
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Stale-answer guards: an answer that comes back after an await is applied
|
|
2
|
+
// only while what it was asked for is still what the page shows. The page
|
|
3
|
+
// switches sessions, reopens a popover or asks again while a fetch is out;
|
|
4
|
+
// an answer for the old ask must be dropped, not drawn over the new one.
|
|
5
|
+
//
|
|
6
|
+
// Two shapes (docs/internals/web-frontend.md, "Stale answers"):
|
|
7
|
+
// - stillOn(get): the thing asked about (the open session's id) hasn't
|
|
8
|
+
// changed. For a one-off read: the same session may ask twice and both
|
|
9
|
+
// answers are good.
|
|
10
|
+
// - createLatest(get): also only the newest ask counts. For a list that is
|
|
11
|
+
// re-read (a refresh while a refresh is out): an older answer landing
|
|
12
|
+
// last would draw the older list.
|
|
13
|
+
//
|
|
14
|
+
// const latest = createLatest(() => sessionId);
|
|
15
|
+
// async function refresh() {
|
|
16
|
+
// const fresh = latest.begin();
|
|
17
|
+
// const data = await api.list(sessionId);
|
|
18
|
+
// if (!fresh()) return;
|
|
19
|
+
// …
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
// @param get () => the key the answer is for (the open session's id)
|
|
23
|
+
// @param at the key asked for (default: get() now)
|
|
24
|
+
// @return () => true while get() is still +at+
|
|
25
|
+
export function stillOn(get, at = get()) {
|
|
26
|
+
return () => get() === at;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// @param get () => the key the asks are for (default: none, the newest ask
|
|
30
|
+
// alone decides)
|
|
31
|
+
// @return {begin(), drop()}: begin() starts an ask and gives its guard,
|
|
32
|
+
// () => true while it is the newest ask and get() is unchanged; drop()
|
|
33
|
+
// makes every ask out stale (a reset)
|
|
34
|
+
export function createLatest(get = () => null) {
|
|
35
|
+
let seq = 0;
|
|
36
|
+
return {
|
|
37
|
+
begin() {
|
|
38
|
+
const mine = ++seq;
|
|
39
|
+
const same = stillOn(get);
|
|
40
|
+
return () => mine === seq && same();
|
|
41
|
+
},
|
|
42
|
+
drop() {
|
|
43
|
+
seq += 1;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// refresh() fetch the list (no events stream)
|
|
20
20
|
// select(id) open a session
|
|
21
21
|
// confirmDelete(id), setArchived(id, archive), archiveOne(id, archive)
|
|
22
|
-
//
|
|
22
|
+
// updateSessionTitle(), showToast(text, action)
|
|
23
23
|
// allViewOpen(), openAllView(), leaveAllView(hash)
|
|
24
24
|
// renameShown(owner) whether a card's title has its ✎ (session_title.js)
|
|
25
25
|
// onRename() the active card's ✎ clicked (session_rename.js open)
|
|
@@ -231,7 +231,7 @@ function draw(el, template) {
|
|
|
231
231
|
export function createListsView(deps) {
|
|
232
232
|
const {
|
|
233
233
|
doc, win, topStripEl, allListEl, sessions, selected, scope, scopeError, eventsLive, refresh, select,
|
|
234
|
-
confirmDelete, setArchived, archiveOne,
|
|
234
|
+
confirmDelete, setArchived, archiveOne, updateSessionTitle, showToast,
|
|
235
235
|
allViewOpen, openAllView, leaveAllView, renameShown = () => false, onRename = () => {}, renaming = () => false,
|
|
236
236
|
} = deps;
|
|
237
237
|
const $ = (s) => doc.querySelector(s);
|
|
@@ -302,18 +302,25 @@ export function createListsView(deps) {
|
|
|
302
302
|
// is the cards drawn, which select mode picks from.
|
|
303
303
|
const drawnOrder = new Map(); // list element → card ids in the order last drawn
|
|
304
304
|
const heldLists = new Set(); // lists drawn in a held order
|
|
305
|
+
// Lists whose last draw was hidden (the strip under the all view, which
|
|
306
|
+
// the store's list write draws as the view opens): that order was never
|
|
307
|
+
// seen, so a hover that stuck on the strip doesn't hold it afterwards.
|
|
308
|
+
const drawnHidden = new Set();
|
|
305
309
|
const canHover = win.matchMedia?.("(hover: hover)").matches ?? false;
|
|
306
310
|
function listOrder(el, next, idOf = (s) => s.id) {
|
|
307
311
|
// The strip's popover holds it too (the pointer leaves the strip for it,
|
|
308
312
|
// and a touch screen has no hover at all), and so does a rename typed
|
|
309
313
|
// over its active card (the card doesn't jump away under the input).
|
|
310
|
-
const
|
|
314
|
+
const shown = el.getClientRects().length > 0;
|
|
315
|
+
const holding = shown
|
|
311
316
|
&& ((canHover && el.matches(":hover")) || (el === topStripEl && (familyPop.isOpen() || renaming())) || chainPopIn(el));
|
|
312
|
-
const held = holding ? drawnOrder.get(el) : null;
|
|
317
|
+
const held = holding && !drawnHidden.has(el) ? drawnOrder.get(el) : null;
|
|
313
318
|
const ordered = held ? heldOrder(held, next, idOf) : next;
|
|
314
319
|
if (held && ordered.some((x, i) => idOf(x) !== idOf(next[i]))) heldLists.add(el);
|
|
315
320
|
else heldLists.delete(el);
|
|
316
321
|
drawnOrder.set(el, ordered.map(idOf));
|
|
322
|
+
if (shown) drawnHidden.delete(el);
|
|
323
|
+
else drawnHidden.add(el);
|
|
317
324
|
return ordered;
|
|
318
325
|
}
|
|
319
326
|
|
|
@@ -498,7 +505,7 @@ export function createListsView(deps) {
|
|
|
498
505
|
noteChainFamilies(cards);
|
|
499
506
|
const refused = scopeError();
|
|
500
507
|
if (refused) {
|
|
501
|
-
draw(topStripEl, html`<div class="hint scope-error">${refused} · <a href=${allScopeHref(""
|
|
508
|
+
draw(topStripEl, html`<div class="hint scope-error">${refused} · <a href=${allScopeHref("")}>show every session</a></div>`);
|
|
502
509
|
lastDrawn.delete(topStripEl);
|
|
503
510
|
familyPop.close();
|
|
504
511
|
chainPop.close();
|
|
@@ -727,14 +734,9 @@ export function createListsView(deps) {
|
|
|
727
734
|
selecting.running = { done: 0, total: ids.length, archive };
|
|
728
735
|
updateSelectBar();
|
|
729
736
|
try {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
render();
|
|
734
|
-
updateInfoBar();
|
|
735
|
-
}
|
|
736
|
-
return answer;
|
|
737
|
-
}, (done) => {
|
|
737
|
+
// An archived one flips in the store's list (archiveOne): the lists
|
|
738
|
+
// and the bar redraw from there.
|
|
739
|
+
const outcome = await runBatch(ids, (id) => archiveOne(id, archive), (done) => {
|
|
738
740
|
selecting.running.done = done;
|
|
739
741
|
updateSelectBar();
|
|
740
742
|
});
|
|
@@ -105,6 +105,8 @@ export function createLiveTiming({
|
|
|
105
105
|
|
|
106
106
|
start(startedAt = new Date(now()).toISOString(), id = null) {
|
|
107
107
|
stopTicker();
|
|
108
|
+
// In place, on purpose: the running turn's own clock in the session
|
|
109
|
+
// store's timing (session_fields.js, D8).
|
|
108
110
|
timing().activeTurn = id ? { id, started_at: startedAt } : { started_at: startedAt };
|
|
109
111
|
turnId = id || null;
|
|
110
112
|
// The turns ended so far, so this one comes next.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// steps' texts.
|
|
9
9
|
const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
|
|
10
10
|
// The closed steps' texts still shown raw: a live turn's (a reload's come
|
|
11
|
-
// rendered,
|
|
11
|
+
// rendered, turn_block.js textTemplate).
|
|
12
12
|
const RAW_STEPS = ".gen:not(.live) > .gen-text:not(.markdown)";
|
|
13
13
|
|
|
14
14
|
export function createMarkdownPlace({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// chi web's JSON as notifications.js and push_devices.js call it:
|
|
2
|
+
// (method, path, body). Over the page's api (data.js), so a signed-out
|
|
3
|
+
// proxy answer (a 401, a redirect, HTML) reports auth_lost as every other
|
|
4
|
+
// read does, and a refusal throws api()'s Error (the server's words, .status,
|
|
5
|
+
// .code).
|
|
6
|
+
|
|
7
|
+
import { api as pageApi } from "./data.js";
|
|
8
|
+
|
|
9
|
+
// @param body the JSON body (undefined: none)
|
|
10
|
+
// @param opts api()'s fetch options (specs: fetchImpl); api: another client
|
|
11
|
+
// @return a Promise of the answer's JSON ({} for an empty one)
|
|
12
|
+
export function methodApi(method, path, body, { api = pageApi, ...opts } = {}) {
|
|
13
|
+
return api(path, { ...opts, method, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
14
|
+
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
isWaitingTag, openPushKeys, pushSupported, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe,
|
|
37
37
|
} from "./push.js";
|
|
38
38
|
import { createPushDevices } from "./push_devices.js";
|
|
39
|
+
import { methodApi } from "./method_api.js";
|
|
39
40
|
|
|
40
41
|
export const NOTIFY_KEY = "chi_notify";
|
|
41
42
|
// The device id chi web gave this app's subscription (POST subscribe's
|
|
@@ -64,17 +65,9 @@ const PUSH_TITLES = {
|
|
|
64
65
|
denied: "Notifications are blocked for this app in the phone's settings",
|
|
65
66
|
};
|
|
66
67
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
method,
|
|
71
|
-
headers: body === undefined ? {} : { "Content-Type": "application/json" },
|
|
72
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
73
|
-
});
|
|
74
|
-
const data = res.status === 204 ? null : await res.json().catch(() => null);
|
|
75
|
-
if (!res.ok) throw new Error(data?.detail || `HTTP ${res.status}`);
|
|
76
|
-
return data;
|
|
77
|
-
}
|
|
68
|
+
// chi web's JSON, or api()'s Error; a signed-out answer reports auth_lost
|
|
69
|
+
// (method_api.js).
|
|
70
|
+
const defaultApi = (method, path, body) => methodApi(method, path, body);
|
|
78
71
|
|
|
79
72
|
// How a presence report goes: a beacon when the page is leaving (it
|
|
80
73
|
// outlives the page; same origin, so the cookie goes), else the api's
|
|
@@ -171,6 +171,16 @@ function scheduleReload() {
|
|
|
171
171
|
reloadTimer = setTimeout(load, 120);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// The types the participant stream sends: exactly Web::ParticipantView::EVENTS
|
|
175
|
+
// (spec/web/participant_view_spec.rb reads this list), so a type the server
|
|
176
|
+
// starts sending isn't dropped here unseen.
|
|
177
|
+
const STREAM_EVENTS = [
|
|
178
|
+
"generation_chunk", "generation_retrying", "tool_call_started", "tool_call_completed",
|
|
179
|
+
"turn_started", "turn_completed", "turn_canceled", "turn_failed", "turn_enqueued", "turn_dequeued", "input_merged",
|
|
180
|
+
"question_requested", "question_answered", "question_cancelled", "continue_offered", "continue_resolved",
|
|
181
|
+
"snapshot", "reset", "verdict_recorded",
|
|
182
|
+
];
|
|
183
|
+
|
|
174
184
|
// The stream from the review data's cursor; a closed or refused stream is
|
|
175
185
|
// opened again from fresh data (a worker may have started or restarted).
|
|
176
186
|
function connect() {
|
|
@@ -203,12 +213,7 @@ function connect() {
|
|
|
203
213
|
renderLive();
|
|
204
214
|
}
|
|
205
215
|
};
|
|
206
|
-
for (const type of
|
|
207
|
-
"turn_started", "turn_completed", "turn_canceled", "turn_failed", "turn_enqueued", "turn_dequeued", "input_merged",
|
|
208
|
-
"question_requested", "question_answered", "question_cancelled", "continue_offered", "continue_resolved",
|
|
209
|
-
"snapshot", "reset", "verdict_recorded"]) {
|
|
210
|
-
source.addEventListener(type, onEvent);
|
|
211
|
-
}
|
|
216
|
+
for (const type of STREAM_EVENTS) source.addEventListener(type, onEvent);
|
|
212
217
|
source.onerror = () => {
|
|
213
218
|
if (source.readyState !== EventSource.CLOSED) return;
|
|
214
219
|
scheduleRetry(retryDelay(failures));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The open session's participants by author ("p:<id>" → name), for the
|
|
2
2
|
// author badges on their prompts (turn_events.js promptOps, a reload's
|
|
3
|
-
//
|
|
3
|
+
// turn_block.js): the participants panel sets it when it reads the list. A
|
|
4
4
|
// prompt's own author_name (a saved message, turn_started) comes first; a
|
|
5
5
|
// badge drawn before the list came, with neither, says UNNAMED until
|
|
6
6
|
// nameUnnamedBadges names it.
|