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
|
@@ -43,9 +43,14 @@ import { renderInfoBar } from "./info_bar.js";
|
|
|
43
43
|
import { sessionBar, workerBadgeFor } from "./info_bar_model.js";
|
|
44
44
|
import { createShownTexts } from "./shown_texts.js";
|
|
45
45
|
import { emptySessionState, sessionStateFrom } from "./session_state.js";
|
|
46
|
+
import { sessionFieldsFrom } from "./session_payload.js";
|
|
47
|
+
import { fieldsFromCard } from "./session_fields.js";
|
|
48
|
+
import { createSessionStore } from "./session_store.js";
|
|
49
|
+
import { createFeatureGates } from "./feature_gates.js";
|
|
50
|
+
import { stillOn } from "./latest.js";
|
|
46
51
|
import { thumbsTemplate } from "./images.js";
|
|
47
|
-
import { normalize, failedTurnText, cantText,
|
|
48
|
-
import {
|
|
52
|
+
import { normalize, failedTurnText, cantText, withoutWorker } from "./format.js";
|
|
53
|
+
import { turnHistoryNodes } from "./turn_block.js";
|
|
49
54
|
import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
50
55
|
import { createBubbles, hintEl, outputBubbleTemplate } from "./bubbles.js";
|
|
51
56
|
import { createCardsPlace } from "./cards_place.js";
|
|
@@ -69,6 +74,7 @@ import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
|
69
74
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
70
75
|
import { copyText, installCopy } from "./copy.js";
|
|
71
76
|
import { installAnswerFold, sweepAnswerFolds } from "./answer_fold.js";
|
|
77
|
+
import { answerLabel, installReviseStacks, reviseChipTemplate, reviseWorkerUnavailable } from "./revise_stack.js";
|
|
72
78
|
import { createQuestionCards } from "./question_cards.js";
|
|
73
79
|
import { splitSnapshotCards } from "./card.js";
|
|
74
80
|
import { PROMPT_LINE_PX, PROMPT_MIN_PX } from "./composer_size.js";
|
|
@@ -153,8 +159,8 @@ const contextChips = createContextChips({
|
|
|
153
159
|
// /llm-context runs in the session's worker.
|
|
154
160
|
const llmContextControl = createLlmContextControl({
|
|
155
161
|
el: infoTextEl,
|
|
156
|
-
summary: () => (selected ? current.llmContext : null),
|
|
157
|
-
session: () => selected,
|
|
162
|
+
summary: () => (store.selected ? store.current.llmContext : null),
|
|
163
|
+
session: () => store.selected,
|
|
158
164
|
running: () => liveTurn.running,
|
|
159
165
|
run: (line) => runSessionCommand(line),
|
|
160
166
|
});
|
|
@@ -162,8 +168,8 @@ const llmContextControl = createLlmContextControl({
|
|
|
162
168
|
// in the session's worker.
|
|
163
169
|
const thinkingControl = createThinkingControl({
|
|
164
170
|
el: infoTextEl,
|
|
165
|
-
summary: () => (selected ? current.thinking : null),
|
|
166
|
-
session: () => selected,
|
|
171
|
+
summary: () => (store.selected ? store.current.thinking : null),
|
|
172
|
+
session: () => store.selected,
|
|
167
173
|
running: () => liveTurn.running,
|
|
168
174
|
run: (line) => runSessionCommand(line),
|
|
169
175
|
});
|
|
@@ -171,8 +177,8 @@ const thinkingControl = createThinkingControl({
|
|
|
171
177
|
// chat host's only): /reasoning-history runs in the session's worker.
|
|
172
178
|
const reasoningHistoryControl = createReasoningHistoryControl({
|
|
173
179
|
el: infoTextEl,
|
|
174
|
-
summary: () => (selected ? current.reasoningHistory : null),
|
|
175
|
-
session: () => selected,
|
|
180
|
+
summary: () => (store.selected ? store.current.reasoningHistory : null),
|
|
181
|
+
session: () => store.selected,
|
|
176
182
|
running: () => liveTurn.running,
|
|
177
183
|
run: (line) => runSessionCommand(line),
|
|
178
184
|
});
|
|
@@ -180,10 +186,10 @@ const reasoningHistoryControl = createReasoningHistoryControl({
|
|
|
180
186
|
// (Invite, revoke, caps, the participant view).
|
|
181
187
|
const participantsControl = createParticipantsControl({
|
|
182
188
|
el: infoTextEl,
|
|
183
|
-
session: () => selected,
|
|
189
|
+
session: () => store.selected,
|
|
184
190
|
api: { list: listParticipants, invite: inviteParticipant, revoke: revokeParticipant, cap: setParticipantCap },
|
|
185
191
|
startReview: (opts) => sessionActions.reviewSelected(opts),
|
|
186
|
-
reviewing: () => sessionActions.reviewing.has(selected),
|
|
192
|
+
reviewing: () => sessionActions.reviewing.has(store.selected),
|
|
187
193
|
// An older prompt drawn before the list came says "a participant": named now.
|
|
188
194
|
onNames: () => {
|
|
189
195
|
nameUnnamedBadges(document);
|
|
@@ -192,7 +198,7 @@ const participantsControl = createParticipantsControl({
|
|
|
192
198
|
return badge && !badge.unnamed ? badge.name : null;
|
|
193
199
|
});
|
|
194
200
|
},
|
|
195
|
-
onChange: () =>
|
|
201
|
+
onChange: () => store.touch("infoBar"),
|
|
196
202
|
toast: (text, action, options) => showToast(text, action, options),
|
|
197
203
|
});
|
|
198
204
|
// The start page's model picker, its ⚙ panel and 🧰 list with the override chips
|
|
@@ -210,10 +216,10 @@ const startPage = createStartPage({
|
|
|
210
216
|
reviewEl: $("#reviewNew"),
|
|
211
217
|
toolkitBtnEl: $("#toolkitBtn"),
|
|
212
218
|
toolkitEl: $("#toolkitNew"),
|
|
213
|
-
selected: () => selected,
|
|
214
|
-
state: () => current,
|
|
219
|
+
selected: () => store.selected,
|
|
220
|
+
state: () => store.current,
|
|
215
221
|
running: () => liveTurn.running,
|
|
216
|
-
sessions: () =>
|
|
222
|
+
sessions: () => store.sessions,
|
|
217
223
|
run: (line) => runSessionCommand(line),
|
|
218
224
|
focusPrompt: () => focusPrompt(),
|
|
219
225
|
toast: (text) => showToast(text),
|
|
@@ -238,8 +244,65 @@ const attachInput = $("#attachInput");
|
|
|
238
244
|
const infoCancelBtn = $("#infoCancelBtn");
|
|
239
245
|
const composeHintEl = $("#composeHint");
|
|
240
246
|
|
|
241
|
-
|
|
247
|
+
// The open session (store.selected), its fields (store.current: session_state.js;
|
|
248
|
+
// forgetSession() starts them over) and every card (store.sessions, the hub's
|
|
249
|
+
// projection): session_store.js. Read store.current fresh after every await.
|
|
250
|
+
// A write redraws what reads it (the redraws are registered below, before
|
|
251
|
+
// the page's first draw); an input the store doesn't hold calls
|
|
252
|
+
// store.touch(name).
|
|
253
|
+
const store = createSessionStore({ initial: emptySessionState() });
|
|
254
|
+
// The page actions the open session's worker must support (feature_gates.js),
|
|
255
|
+
// the store's featureGates redraw: a gate's redraw runs when its answer
|
|
256
|
+
// flips (a restart brings the feature, the worker goes). forgetSession and
|
|
257
|
+
// renderSessionView prime them: the history they draw has the answer already.
|
|
258
|
+
const featureGates = createFeatureGates([
|
|
259
|
+
// Verify fresh on a verdict's card.
|
|
260
|
+
{
|
|
261
|
+
name: "verify",
|
|
262
|
+
unavailable: (cur) => verifyWorkerUnavailable(cur.workerFeatures, cur.bridgeUp),
|
|
263
|
+
redraw: () => redrawFresh(historyEl, verifyFreshState()),
|
|
264
|
+
},
|
|
265
|
+
// Revise on an answer and the composer's Revising chip (revise_stack.js).
|
|
266
|
+
{
|
|
267
|
+
name: "revise",
|
|
268
|
+
unavailable: (cur) => reviseWorkerUnavailable(cur.workerFeatures, cur.bridgeUp),
|
|
269
|
+
redraw: () => redrawRevise(),
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
featureGates.prime(store.current);
|
|
273
|
+
// The open session's id, for the stale-answer guards (latest.js stillOn).
|
|
274
|
+
const openId = () => store.selected;
|
|
242
275
|
let pendingVerifyAnchor = null;
|
|
276
|
+
// The composer's armed Revise ({turn_id, label}: the answer the next
|
|
277
|
+
// message rewrites), shown as its chip; Verify's lifecycle (an emptied
|
|
278
|
+
// composer, another session, the send's ack let go of it). Set only
|
|
279
|
+
// through setReviseAnchor (it redraws the chip and the answers' buttons).
|
|
280
|
+
let pendingReviseAnchor = null;
|
|
281
|
+
let reviseStacks = null;
|
|
282
|
+
const reviseChipEl = $("#reviseChip");
|
|
283
|
+
const reviseUnavailable = () => reviseWorkerUnavailable(store.current.workerFeatures, store.current.bridgeUp);
|
|
284
|
+
function redrawRevise() {
|
|
285
|
+
if (reviseChipEl) {
|
|
286
|
+
render(reviseChipTemplate(pendingReviseAnchor, {
|
|
287
|
+
unavailable: reviseUnavailable(),
|
|
288
|
+
onClear: () => { setReviseAnchor(null); focusPrompt(); },
|
|
289
|
+
}), reviseChipEl);
|
|
290
|
+
reviseChipEl.hidden = !pendingReviseAnchor;
|
|
291
|
+
}
|
|
292
|
+
reviseStacks?.decorate();
|
|
293
|
+
}
|
|
294
|
+
function setReviseAnchor(anchor) {
|
|
295
|
+
if (anchor === pendingReviseAnchor) return;
|
|
296
|
+
pendingReviseAnchor = anchor;
|
|
297
|
+
redrawRevise();
|
|
298
|
+
}
|
|
299
|
+
// A restored Revise turn's draft ({turn_id}): armed again, its label from
|
|
300
|
+
// the answer it names.
|
|
301
|
+
function restoreRevise(anchor) {
|
|
302
|
+
if (!anchor?.turn_id) return;
|
|
303
|
+
const answer = historyEl.querySelector(`.bubble.output[data-answer-turn="${CSS.escape(anchor.turn_id)}"]`);
|
|
304
|
+
setReviseAnchor({ turn_id: anchor.turn_id, label: answerLabel(answer) });
|
|
305
|
+
}
|
|
243
306
|
let annotateBar = null;
|
|
244
307
|
// The session the composer's text was last written in ("" the start page,
|
|
245
308
|
// undefined unknown): the composer is one for every session, and continue →
|
|
@@ -248,7 +311,7 @@ let draftFrom;
|
|
|
248
311
|
// Record that the composer's text was written now, in session +id+ (default
|
|
249
312
|
// the open one). Every write of text into the composer comes through here.
|
|
250
313
|
function noteDraft(id) {
|
|
251
|
-
draftFrom = id ?? selected ?? "";
|
|
314
|
+
draftFrom = id ?? store.selected ?? "";
|
|
252
315
|
}
|
|
253
316
|
let streamControl = null;
|
|
254
317
|
// The session list's stream (GET /api/events); null while the page falls
|
|
@@ -258,31 +321,14 @@ let eventsControl = null;
|
|
|
258
321
|
// session while one ran (its bridge_up event): the tail runs once more.
|
|
259
322
|
let resyncsInFlight = 0;
|
|
260
323
|
let resyncAgain = false;
|
|
261
|
-
let allSessions = [];
|
|
262
324
|
// The running turn's texts on the page (the turn output dedupe).
|
|
263
325
|
const shownTexts = createShownTexts();
|
|
264
|
-
// The page's view of a turn (web.view, or ?view=stage|turn): the running
|
|
265
|
-
// turn pinned above the composer (the stage, stage_view.js), or one block
|
|
266
|
-
// per turn with its steps in the history (turn_view.js).
|
|
267
|
-
const view = ["stage", "turn"].includes(document.body.dataset.view) ? document.body.dataset.view : "stage";
|
|
268
|
-
// The running turn's DOM: the turn block, or the stage (the turn block in
|
|
269
|
-
// its cloud).
|
|
270
|
-
const viewArgs = {
|
|
271
|
-
historyEl,
|
|
272
|
-
appendToHistory: (el) => appendToHistory(el),
|
|
273
|
-
isNearBottom: () => isNearBottom(),
|
|
274
|
-
sawText: (k) => shownTexts.saw(k),
|
|
275
|
-
sessionId: () => selected,
|
|
276
|
-
// The stop-task button on a running task_wait, for a worker that has
|
|
277
|
-
// the route (its sidecar features).
|
|
278
|
-
taskStop: createTaskStopper({ sessionId: () => selected, features: () => current.workerFeatures }),
|
|
279
|
-
};
|
|
280
326
|
const markdownPlace = createMarkdownPlace({
|
|
281
327
|
capabilities: () => pageCapabilities,
|
|
282
328
|
renderText,
|
|
283
329
|
roots: () => turnRoots(),
|
|
284
330
|
touchedFiles,
|
|
285
|
-
currentDir: () => current.dir,
|
|
331
|
+
currentDir: () => store.current.dir,
|
|
286
332
|
linkRefs,
|
|
287
333
|
applyFinalAnswer,
|
|
288
334
|
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
@@ -295,28 +341,38 @@ const { renderMarkdown, pairRefsIn, linkRefsIn, applyFinalMarkdown, linkTextRefs
|
|
|
295
341
|
// The stage view's working set (web.working_set, or ?working_set=1|0):
|
|
296
342
|
// what each turn brought into context, in a rail in the stage, a footprint
|
|
297
343
|
// under each finished turn and a "set N" chip in the mem chip's place
|
|
298
|
-
// (working_set_view.js).
|
|
299
|
-
const workingSet =
|
|
344
|
+
// (working_set_view.js).
|
|
345
|
+
const workingSet = document.body.dataset.workingSet === "1"
|
|
300
346
|
? createWorkingSet({
|
|
301
|
-
historyEl, stageRoot: () => turnDom.root, state: () => current, session: () => selected, infoEl: infoTextEl,
|
|
302
|
-
onChange: () =>
|
|
347
|
+
historyEl, stageRoot: () => turnDom.root, state: () => store.current, session: () => store.selected, infoEl: infoTextEl,
|
|
348
|
+
onChange: () => store.touch("infoBar"), getWorkingSet,
|
|
303
349
|
// A drop or undrop from the list: the session's command, as typed.
|
|
304
|
-
sendLine: (line) => (selected ? sendCommand(selected, line, { clientId }) : null),
|
|
350
|
+
sendLine: (line) => (store.selected ? sendCommand(store.selected, line, { clientId }) : null),
|
|
305
351
|
})
|
|
306
352
|
: null;
|
|
307
353
|
document.body.classList.toggle("working-set", !!workingSet);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
:
|
|
354
|
+
// The running turn's DOM: the stage pinned above the composer
|
|
355
|
+
// (stage_view.js), the turn block (turn_block.js) in its cloud.
|
|
356
|
+
const turnDom = createStageView({
|
|
357
|
+
historyEl,
|
|
358
|
+
isNearBottom: () => atHistoryEnd(),
|
|
359
|
+
sawText: (k) => shownTexts.saw(k),
|
|
360
|
+
sessionId: () => store.selected,
|
|
361
|
+
// The stop-task button on a running task_wait, for a worker that has
|
|
362
|
+
// the route (its sidecar features).
|
|
363
|
+
taskStop: createTaskStopper({ sessionId: () => store.selected, features: () => store.current.workerFeatures }),
|
|
364
|
+
dockEl: $("#dock"),
|
|
365
|
+
centerEl: $("#center"),
|
|
366
|
+
renderMarkdown,
|
|
367
|
+
linkRefs: linkRefsIn,
|
|
368
|
+
workingSet,
|
|
369
|
+
});
|
|
314
370
|
// The history's bottom reserve and the toast's place follow the composer
|
|
315
371
|
// card and the stage floating over the history (dock_reserve.js).
|
|
316
372
|
const { placeToast } = createDockReserve({
|
|
317
373
|
dockEl: $("#dock"),
|
|
318
374
|
centerEl: $("#center"),
|
|
319
|
-
stageEl:
|
|
375
|
+
stageEl: turnDom.root,
|
|
320
376
|
historyEl,
|
|
321
377
|
toastEl: $("#toast"),
|
|
322
378
|
modelPanelEl: $("#modelPanel"),
|
|
@@ -325,29 +381,26 @@ const { placeToast } = createDockReserve({
|
|
|
325
381
|
// Where a running turn's rows can be: the history, and the stage while it
|
|
326
382
|
// holds a turn (lookups by id, copy, annotate, card actions).
|
|
327
383
|
function turnRoots() {
|
|
328
|
-
return
|
|
384
|
+
return [historyEl, turnDom.root];
|
|
329
385
|
}
|
|
330
|
-
const systemOneWrongAction = createSystemOneWrongAction({ sessionId: () => selected });
|
|
386
|
+
const systemOneWrongAction = createSystemOneWrongAction({ sessionId: () => store.selected });
|
|
331
387
|
turnRoots().forEach((root) => root.addEventListener("click", systemOneWrongAction));
|
|
332
388
|
function findInTurnRoots(selector) {
|
|
333
389
|
return turnRoots().flatMap((root) => [...root.querySelectorAll(selector)]);
|
|
334
390
|
}
|
|
335
|
-
// The open session's fields (session_state.js); resetSessionState() starts
|
|
336
|
-
// them over.
|
|
337
|
-
let current = emptySessionState();
|
|
338
391
|
// The running turn as the page follows it (live_turn.js): whether one
|
|
339
392
|
// runs, its timing line and the turns this page saw end (live_timing.js),
|
|
340
393
|
// its merges' bubble flags, its turn-end reads.
|
|
341
394
|
const liveTurn = createLiveTurn({
|
|
342
|
-
timing: () => current.timing,
|
|
395
|
+
timing: () => store.current.timing,
|
|
343
396
|
// The stage's status row while it holds the turn (the line moves into
|
|
344
397
|
// the history with it), else the history.
|
|
345
398
|
place: (el) => {
|
|
346
|
-
const stageHost = turnDom.timingHost
|
|
399
|
+
const stageHost = turnDom.timingHost();
|
|
347
400
|
(stageHost || historyEl).appendChild(el);
|
|
348
401
|
return !!stageHost;
|
|
349
402
|
},
|
|
350
|
-
onTick: () =>
|
|
403
|
+
onTick: () => store.touch("infoBar"),
|
|
351
404
|
});
|
|
352
405
|
|
|
353
406
|
// The composer's delivery line while the open session's turn runs
|
|
@@ -356,22 +409,22 @@ const liveTurn = createLiveTurn({
|
|
|
356
409
|
const deliveryControl = createDeliveryControl({
|
|
357
410
|
el: $("#deliveryLine"),
|
|
358
411
|
read: () => {
|
|
359
|
-
const turn = turnDom.current
|
|
412
|
+
const turn = turnDom.current().turn || null;
|
|
360
413
|
const running = turn ? turn.gens.flatMap((g) => g.tools).reverse().find((r) => r.status === "running") : null;
|
|
361
414
|
return {
|
|
362
|
-
phase: turnDom.phase
|
|
415
|
+
phase: turnDom.phase() || liveSlots(turn).phase,
|
|
363
416
|
gen: turn ? currentGen(turn) : null,
|
|
364
417
|
tool: running ? { name: toolName(running), startedAt: running.startedAt ?? null } : null,
|
|
365
418
|
cutAfter: liveTurn.cutAfter,
|
|
366
419
|
serverGen: liveTurn.serverGen,
|
|
367
|
-
canQueue: current.workerFeatures.includes("delivery"),
|
|
368
|
-
model: current.model,
|
|
420
|
+
canQueue: store.current.workerFeatures.includes("delivery"),
|
|
421
|
+
model: store.current.model,
|
|
369
422
|
participantTurn: !!liveTurn.participantTurn,
|
|
370
423
|
};
|
|
371
424
|
},
|
|
372
425
|
composerEmpty: () => !composer.hasDraft(),
|
|
373
426
|
send: (delivery) => handleSendTurn({ delivery }),
|
|
374
|
-
session: () => selected,
|
|
427
|
+
session: () => store.selected,
|
|
375
428
|
});
|
|
376
429
|
|
|
377
430
|
// This tab's id in the session's events: tells its own prompts from the
|
|
@@ -379,7 +432,7 @@ const deliveryControl = createDeliveryControl({
|
|
|
379
432
|
const clientId = newClientId();
|
|
380
433
|
// The question and continue cards in the history (question_cards.js).
|
|
381
434
|
const questionCards = createQuestionCards({
|
|
382
|
-
sessionId: () => selected,
|
|
435
|
+
sessionId: () => store.selected,
|
|
383
436
|
clientId,
|
|
384
437
|
place: (card) => appendToHistory(card),
|
|
385
438
|
isNearBottom: () => isNearBottom(),
|
|
@@ -400,7 +453,7 @@ const bubbles = createBubbles({
|
|
|
400
453
|
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
401
454
|
shownTexts,
|
|
402
455
|
liveTurn,
|
|
403
|
-
selected: () => selected,
|
|
456
|
+
selected: () => store.selected,
|
|
404
457
|
clientId,
|
|
405
458
|
});
|
|
406
459
|
const {
|
|
@@ -415,7 +468,7 @@ const cards = createCardsPlace({
|
|
|
415
468
|
historyEl,
|
|
416
469
|
turnDom,
|
|
417
470
|
findInTurnRoots: (selector) => findInTurnRoots(selector),
|
|
418
|
-
selected: () => selected,
|
|
471
|
+
selected: () => store.selected,
|
|
419
472
|
clientId,
|
|
420
473
|
questionCards,
|
|
421
474
|
bubbles,
|
|
@@ -450,23 +503,22 @@ let foldsSwept = false;
|
|
|
450
503
|
function sweepFoldsOnce() {
|
|
451
504
|
if (foldsSwept) return;
|
|
452
505
|
foldsSwept = true;
|
|
453
|
-
sweepAnswerFolds(
|
|
506
|
+
sweepAnswerFolds(store.sessions, { scoped: !!scope.dir });
|
|
454
507
|
}
|
|
455
508
|
|
|
456
509
|
// The list by fetching: the fallback when there is no events stream (a
|
|
457
510
|
// chi web without a hub, or ?dir the server refused: the 400 is read here).
|
|
458
511
|
async function refresh() {
|
|
459
512
|
try {
|
|
460
|
-
|
|
461
|
-
notifications.noticeList(
|
|
513
|
+
store.update({ sessions: await listSessions("updated_at", "desc", { dir: scope.dir }) });
|
|
514
|
+
notifications.noticeList(store.sessions);
|
|
462
515
|
sweepFoldsOnce();
|
|
463
516
|
} catch (e) {
|
|
464
517
|
if (e.status !== 400 || !scope.dir) throw e;
|
|
465
518
|
scopeError = e.message;
|
|
466
|
-
|
|
519
|
+
store.update({ sessions: [] });
|
|
467
520
|
renderScope();
|
|
468
521
|
}
|
|
469
|
-
lists.render();
|
|
470
522
|
}
|
|
471
523
|
|
|
472
524
|
// The list as a projection of the hub's events: a snapshot on every
|
|
@@ -474,35 +526,27 @@ async function refresh() {
|
|
|
474
526
|
// worker follow its events (a new worker's bridge_up reconnects the stream).
|
|
475
527
|
function openSessionEvents() {
|
|
476
528
|
eventsControl = openEvents({
|
|
477
|
-
|
|
529
|
+
// The list redraws the lists and the bar (its children chip, its chain's
|
|
530
|
+
// ← → links): store.update({sessions}).
|
|
531
|
+
snapshot: (data) => store.batch(() => {
|
|
478
532
|
noticeVersions(data);
|
|
479
533
|
if (updateSheet.reconnected(data.version)) showToast(`chi web reconnected on chi ${data.version}`);
|
|
480
534
|
if (data.chi_update) updateFrame(data.chi_update, { snapshot: true });
|
|
481
535
|
notifications.noticeSessions("snapshot", data);
|
|
482
|
-
|
|
536
|
+
store.update({ sessions: applySessionEvent(store.sessions, "snapshot", data) });
|
|
483
537
|
sweepFoldsOnce();
|
|
484
|
-
|
|
485
|
-
const sess = allSessions.find((s) => s.id === selected);
|
|
538
|
+
const sess = store.sessions.find((s) => s.id === store.selected);
|
|
486
539
|
if (sess) followSelectedSession(sess);
|
|
487
|
-
},
|
|
488
|
-
session: (data) => {
|
|
540
|
+
}),
|
|
541
|
+
session: (data) => store.batch(() => {
|
|
489
542
|
notifications.noticeSessions("session", data);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// Its children chip; its chain's ← → links (a link that continues it, its previous one).
|
|
494
|
-
else if (selected && (data.session?.parent_id === selected || data.session?.continues === selected
|
|
495
|
-
|| (data.session?.id && data.session.id === current.continues))) updateInfoBar();
|
|
496
|
-
},
|
|
543
|
+
store.update({ sessions: applySessionEvent(store.sessions, "session", data) });
|
|
544
|
+
if (data.session && data.session.id === store.selected) followSelectedSession(data.session);
|
|
545
|
+
}),
|
|
497
546
|
session_gone: (data) => {
|
|
498
547
|
notifications.noticeSessions("session_gone", data);
|
|
499
548
|
lists.forget(data.id);
|
|
500
|
-
|
|
501
|
-
if (next !== allSessions) {
|
|
502
|
-
allSessions = next;
|
|
503
|
-
lists.render();
|
|
504
|
-
if (selected) updateInfoBar(); // a deleted child leaves the children chip, a deleted chain link its ← → link
|
|
505
|
-
}
|
|
549
|
+
store.update({ sessions: applySessionEvent(store.sessions, "session_gone", data) });
|
|
506
550
|
leaveGoneSession(data.id, "deleted");
|
|
507
551
|
},
|
|
508
552
|
chi: (data) => noticeVersions(data),
|
|
@@ -548,7 +592,7 @@ function noticeVersions(data) {
|
|
|
548
592
|
const notice = versionNotice({ loaded: document.body.dataset.version, served: versions.served,
|
|
549
593
|
installed: versions.installed, available: versions.available,
|
|
550
594
|
updateBlocked: versions.updateBlocked, shown: versionNoticesShown });
|
|
551
|
-
|
|
595
|
+
store.touch("infoBar"); // the worker badge compares with the newest installed
|
|
552
596
|
if (!notice) return;
|
|
553
597
|
versionNoticesShown.add(notice.key);
|
|
554
598
|
if (notice.key.startsWith("available:")) writeStored(AVAILABLE_SHOWN_KEY, notice.key.slice("available:".length));
|
|
@@ -603,18 +647,13 @@ function copyUpdateCommand(command) {
|
|
|
603
647
|
// (a turn another client sent woke one, or a replacement after a kill)
|
|
604
648
|
// gets the stream attached through one resync.
|
|
605
649
|
function followSelectedSession(sess) {
|
|
606
|
-
current.owner = sess.owner || null;
|
|
607
|
-
current.workerVersion = sess.worker_version || null;
|
|
608
|
-
const verifyWas = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
609
|
-
current.workerFeatures = Array.isArray(sess.worker_features) ? sess.worker_features : [];
|
|
610
|
-
current.bridgeUp = sess.bridge_up === true;
|
|
611
|
-
// The Verify gate flipped (a restart brought the feature): the cards' Verify fresh part follows.
|
|
612
|
-
if (verifyWas !== verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp)) {
|
|
613
|
-
redrawFresh(historyEl, verifyFreshState());
|
|
614
|
-
}
|
|
615
650
|
const streaming = !!(streamControl && streamControl.live);
|
|
616
|
-
|
|
617
|
-
|
|
651
|
+
// A Verify gate that flips (a restart brought the feature) redraws the
|
|
652
|
+
// cards' Verify fresh part: featureGates.
|
|
653
|
+
store.batch(() => {
|
|
654
|
+
store.update({ current: fieldsFromCard(sess) });
|
|
655
|
+
store.setStatus(sess.status, "hub", { streaming });
|
|
656
|
+
});
|
|
618
657
|
if (!sess.bridge_up || streaming) return;
|
|
619
658
|
if (resyncsInFlight > 0) resyncAgain = true;
|
|
620
659
|
else resync();
|
|
@@ -625,7 +664,7 @@ function followSelectedSession(sess) {
|
|
|
625
664
|
// new session starts on the start page.
|
|
626
665
|
function renderScope() {
|
|
627
666
|
const el = $("#scope");
|
|
628
|
-
render(scopeTemplate(scope, { error: scopeError, where: () => ({ hash: location.hash
|
|
667
|
+
render(scopeTemplate(scope, { error: scopeError, where: () => ({ hash: location.hash }) }), el);
|
|
629
668
|
el.hidden = false;
|
|
630
669
|
// The tab: "<session title> · Chi · <project>" (updateSessionTitle).
|
|
631
670
|
tabBase = scope.projectName ? `Chi · ${scope.projectName}` : "Chi";
|
|
@@ -634,7 +673,6 @@ function renderScope() {
|
|
|
634
673
|
|
|
635
674
|
createProjectPicker({
|
|
636
675
|
el: $("#scope"), scope, scopeError: () => scopeError, api,
|
|
637
|
-
where: () => ({ search: location.search }),
|
|
638
676
|
});
|
|
639
677
|
|
|
640
678
|
// The session lists (lists_view.js): the strip, all sessions, their cards,
|
|
@@ -644,8 +682,8 @@ const lists = createListsView({
|
|
|
644
682
|
win: window,
|
|
645
683
|
topStripEl,
|
|
646
684
|
allListEl,
|
|
647
|
-
sessions: () =>
|
|
648
|
-
selected: () => selected,
|
|
685
|
+
sessions: () => store.sessions,
|
|
686
|
+
selected: () => store.selected,
|
|
649
687
|
scope,
|
|
650
688
|
scopeError: () => scopeError,
|
|
651
689
|
eventsLive: () => !!eventsControl,
|
|
@@ -654,7 +692,6 @@ const lists = createListsView({
|
|
|
654
692
|
confirmDelete: (id) => confirmDelete(id),
|
|
655
693
|
setArchived: (id, archive) => setArchived(id, archive),
|
|
656
694
|
archiveOne: (id, archive) => archiveOne(id, archive),
|
|
657
|
-
updateInfoBar: () => updateInfoBar(),
|
|
658
695
|
updateSessionTitle: () => updateSessionTitle(),
|
|
659
696
|
showToast: (text, action) => showToast(text, action),
|
|
660
697
|
allViewOpen: () => allViewOpen(),
|
|
@@ -676,19 +713,17 @@ const stopSheet = createStopSheet({
|
|
|
676
713
|
});
|
|
677
714
|
|
|
678
715
|
const sessionActions = createSessionActions({
|
|
679
|
-
selected: () => selected,
|
|
680
|
-
|
|
681
|
-
sessions: () =>
|
|
682
|
-
setSessions: (list) => {
|
|
716
|
+
selected: () => store.selected,
|
|
717
|
+
store,
|
|
718
|
+
sessions: () => store.sessions,
|
|
719
|
+
setSessions: (list) => store.update({ sessions: list }),
|
|
683
720
|
api: { continueSession, startReview, archiveSession, unarchiveSession, deleteSession, stopSession,
|
|
684
721
|
sessionHolds, stopSessionHolds },
|
|
685
722
|
eventsLive: () => !!eventsControl,
|
|
686
723
|
refresh,
|
|
687
|
-
lists,
|
|
688
724
|
select,
|
|
689
725
|
clearSelection,
|
|
690
726
|
showNotice,
|
|
691
|
-
updateInfoBar,
|
|
692
727
|
toast: showToast,
|
|
693
728
|
addCreatedSession,
|
|
694
729
|
fillOpener,
|
|
@@ -707,7 +742,7 @@ const rename = createRename({
|
|
|
707
742
|
const row = topStripEl.querySelector(".card.active .preview-row");
|
|
708
743
|
return row && row.getClientRects().length ? row : null; // not in the All sessions view's hidden strip
|
|
709
744
|
},
|
|
710
|
-
session: () => selected,
|
|
745
|
+
session: () => store.selected,
|
|
711
746
|
titleOf: (id) => lists.sessionsById().get(id)?.title || null,
|
|
712
747
|
shownTitle: () => shownTitle(),
|
|
713
748
|
closePopovers: () => {
|
|
@@ -741,7 +776,7 @@ function closeAllView() {
|
|
|
741
776
|
if (allViewPushed) {
|
|
742
777
|
history.back();
|
|
743
778
|
} else {
|
|
744
|
-
leaveAllView(sessionHash(selected));
|
|
779
|
+
leaveAllView(sessionHash(store.selected));
|
|
745
780
|
}
|
|
746
781
|
}
|
|
747
782
|
// Leave all sessions for a place of our own (a picked session, a new session):
|
|
@@ -768,7 +803,7 @@ function selectFromHash() {
|
|
|
768
803
|
const claim = claimFromHash(location.hash);
|
|
769
804
|
const id = sessionIdFromHash(location.hash) || claim?.sessionId || null;
|
|
770
805
|
if (claim) pendingClaim = claim;
|
|
771
|
-
if (id && id !== selected) select(id);
|
|
806
|
+
if (id && id !== store.selected) select(id);
|
|
772
807
|
else if (claim) showPendingClaim();
|
|
773
808
|
}
|
|
774
809
|
let pendingClaim = null;
|
|
@@ -781,10 +816,10 @@ const CLAIM_ROOTS = ".bubble.output, .bubble.user .user-message, .gen-text";
|
|
|
781
816
|
// switched), so the same link opened again is a hashchange.
|
|
782
817
|
function showPendingClaim() {
|
|
783
818
|
const claim = pendingClaim;
|
|
784
|
-
if (!claim || claim.sessionId !== selected) return;
|
|
819
|
+
if (!claim || claim.sessionId !== store.selected) return;
|
|
785
820
|
pendingClaim = null;
|
|
786
|
-
if (!allViewOpen() && location.hash !== sessionHash(selected)) {
|
|
787
|
-
history.replaceState(null, "", location.pathname + location.search + sessionHash(selected));
|
|
821
|
+
if (!allViewOpen() && location.hash !== sessionHash(store.selected)) {
|
|
822
|
+
history.replaceState(null, "", location.pathname + location.search + sessionHash(store.selected));
|
|
788
823
|
}
|
|
789
824
|
if (showClaim(historyEl, claim, { roots: CLAIM_ROOTS, opts: claimOpts() })) return;
|
|
790
825
|
historyEl.scrollTop = 0;
|
|
@@ -792,25 +827,25 @@ function showPendingClaim() {
|
|
|
792
827
|
}
|
|
793
828
|
// How the participant page shows this page's text (claim_link.js).
|
|
794
829
|
function claimOpts() {
|
|
795
|
-
return { home: document.body.dataset.home || "", cwd: current.dir || "" };
|
|
830
|
+
return { home: document.body.dataset.home || "", cwd: store.current.dir || "" };
|
|
796
831
|
}
|
|
797
832
|
// Annotate's "Copy link" for a selection in a turn's answer, step or own
|
|
798
833
|
// message: the participant page's link when the session has a person
|
|
799
834
|
// active (it opens in the browser their review link was opened in: one
|
|
800
835
|
// link per browser, the cookie), else this page's own.
|
|
801
836
|
function claimLinkFor(source, range) {
|
|
802
|
-
if (!selected || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)) return null;
|
|
837
|
+
if (!store.selected || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)) return null;
|
|
803
838
|
const turnId = turnIdOf(historyEl, source.root);
|
|
804
839
|
if (!turnId) return null;
|
|
805
840
|
const claim = { turnId, ...selectionClaim(source.root, range, claimOpts()) };
|
|
806
841
|
if (!claim.q) return null;
|
|
807
842
|
const target = participantsControl.claimTarget();
|
|
808
843
|
if (!target) {
|
|
809
|
-
return { url: `${location.origin}${location.pathname}${location.search}${ownerClaimHash(selected, claim)}`,
|
|
844
|
+
return { url: `${location.origin}${location.pathname}${location.search}${ownerClaimHash(store.selected, claim)}`,
|
|
810
845
|
title: "A link to this on your page (no person is invited to this session)" };
|
|
811
846
|
}
|
|
812
847
|
const whose = target.names.map((name) => `${name}'s`).join(" or ");
|
|
813
|
-
return { url: participantClaimLink(target.base, selected, claim),
|
|
848
|
+
return { url: participantClaimLink(target.base, store.selected, claim),
|
|
814
849
|
title: `A link to this for ${whose} browser: it opens where their review link was opened (one link per browser). Opened here, it shows your page.` };
|
|
815
850
|
}
|
|
816
851
|
async function copyClaimLink({ url }) {
|
|
@@ -833,8 +868,8 @@ function applyView() {
|
|
|
833
868
|
lists.familyPop.close(); // the strip is hidden in the all view
|
|
834
869
|
// The list is live; only its order can lag (upserts keep cards in
|
|
835
870
|
// place), so the all view opens in the server's order.
|
|
836
|
-
|
|
837
|
-
|
|
871
|
+
// A new list: the lists' redraw draws the all view (lists.render).
|
|
872
|
+
store.update({ sessions: sortedByUpdated(store.sessions) });
|
|
838
873
|
if (!eventsControl) refresh();
|
|
839
874
|
$("#filter").select();
|
|
840
875
|
}
|
|
@@ -848,6 +883,8 @@ $("#allBack").addEventListener("click", closeAllView);
|
|
|
848
883
|
// A phone's composer fits a few words and has no Shift+Enter: the short
|
|
849
884
|
// placeholder there.
|
|
850
885
|
const narrowScreen = window.matchMedia("(max-width: 600px)");
|
|
886
|
+
// An input the store doesn't hold, with the composer as its only reader: a
|
|
887
|
+
// direct call (the bar doesn't read it).
|
|
851
888
|
narrowScreen.addEventListener("change", () => updateComposerMode());
|
|
852
889
|
function setPlaceholder(long, short) {
|
|
853
890
|
promptEl.placeholder = narrowScreen.matches ? short : long;
|
|
@@ -862,7 +899,7 @@ let hadSession = false;
|
|
|
862
899
|
// its next step (steering). Cancel is its own button then.
|
|
863
900
|
function updateComposerMode() {
|
|
864
901
|
// In a session the box is one line: its minimum follows the class.
|
|
865
|
-
if (document.body.classList.toggle("has-session", !!selected) !== hadSession) {
|
|
902
|
+
if (document.body.classList.toggle("has-session", !!store.selected) !== hadSession) {
|
|
866
903
|
hadSession = !hadSession;
|
|
867
904
|
// Back on the start page: its remembered model, not the last link's.
|
|
868
905
|
if (!hadSession) startPage.restoreStartPick();
|
|
@@ -871,12 +908,12 @@ function updateComposerMode() {
|
|
|
871
908
|
}
|
|
872
909
|
startPage.syncModelPick();
|
|
873
910
|
// The start page's choice was for a new session: opening a session drops it.
|
|
874
|
-
if (selected) startPage.dropChoices();
|
|
875
|
-
const running = liveTurn.running && !!selected;
|
|
911
|
+
if (store.selected) startPage.dropChoices();
|
|
912
|
+
const running = liveTurn.running && !!store.selected;
|
|
876
913
|
cancelBtn.style.display = running ? "" : "none";
|
|
877
914
|
// No images for a session a terminal owns (it takes no message from here);
|
|
878
915
|
// on a phone none while a turn runs either: Stop turn needs the row's room.
|
|
879
|
-
attachBtn.hidden = (!!selected && current.owner === "tui") || (running && narrowScreen.matches);
|
|
916
|
+
attachBtn.hidden = (!!store.selected && store.current.owner === "tui") || (running && narrowScreen.matches);
|
|
880
917
|
syncDeliveryLine();
|
|
881
918
|
if (running) {
|
|
882
919
|
setPlaceholder("Running… a message goes in at the next step · Enter to send · Alt+Enter queues", "Steer the turn…");
|
|
@@ -885,15 +922,14 @@ function updateComposerMode() {
|
|
|
885
922
|
emptyEl.classList.add("hidden");
|
|
886
923
|
return;
|
|
887
924
|
}
|
|
888
|
-
if (!selected) {
|
|
925
|
+
if (!store.selected) {
|
|
889
926
|
setPlaceholder("Ask chi anything…", "Ask chi anything…");
|
|
890
927
|
composeHintEl.textContent = "Enter to start · Shift+Enter for a newline";
|
|
891
928
|
actionBtn.textContent = "Start";
|
|
892
929
|
actionBtn.disabled = false;
|
|
893
930
|
emptyEl.classList.remove("hidden");
|
|
894
931
|
historyEl.replaceChildren();
|
|
895
|
-
|
|
896
|
-
} else if (current.owner === "tui") {
|
|
932
|
+
} else if (store.current.owner === "tui") {
|
|
897
933
|
setPlaceholder("A terminal chi owns this session · run it with chi --shared to send from here", "A terminal chi owns this session");
|
|
898
934
|
composeHintEl.textContent = "";
|
|
899
935
|
actionBtn.textContent = "Send";
|
|
@@ -913,31 +949,28 @@ function updateComposerMode() {
|
|
|
913
949
|
// while idle, where a message goes in while the turn runs.
|
|
914
950
|
function syncDeliveryLine() {
|
|
915
951
|
// A terminal owns the session: the composer is off, so is its row.
|
|
916
|
-
if (!selected || current.owner === "tui") deliveryControl.show("off");
|
|
917
|
-
else deliveryControl.show(liveTurn.running && current.owner !== "tui" ? "running" : "idle");
|
|
952
|
+
if (!store.selected || store.current.owner === "tui") deliveryControl.show("off");
|
|
953
|
+
else deliveryControl.show(liveTurn.running && store.current.owner !== "tui" ? "running" : "idle");
|
|
918
954
|
}
|
|
919
955
|
|
|
920
956
|
function setTurnRunning(running) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
957
|
+
store.batch(() => {
|
|
958
|
+
liveTurn.running = !!running;
|
|
959
|
+
// A fresh link's picker goes now, not at the timing re-read after the turn.
|
|
960
|
+
if (running && store.selected) store.update({ current: { turnStarted: true } });
|
|
961
|
+
// liveTurn.running isn't the store's: the composer and the bar read it.
|
|
962
|
+
store.touch("composer");
|
|
963
|
+
store.touch("infoBar");
|
|
964
|
+
});
|
|
926
965
|
}
|
|
927
966
|
|
|
928
967
|
// Session status is turn state (idle/running): follow the live stream in the
|
|
929
968
|
// info bar and the session's card without a refetch.
|
|
930
969
|
function setLiveStatus(status) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// snapshot or the all view re-sorts (no card jumping under the pointer;
|
|
936
|
-
// a session event replaces it in place too, see sessions_list.js).
|
|
937
|
-
allSessions[i] = withLiveStatus(allSessions[i], status);
|
|
938
|
-
lists.render();
|
|
939
|
-
}
|
|
940
|
-
updateInfoBar();
|
|
970
|
+
// Its card gets the new dot and time (withLiveStatus); it keeps its place
|
|
971
|
+
// until the next snapshot or the all view re-sorts (no card jumping under
|
|
972
|
+
// the pointer; a session event replaces it in place too, see sessions_list.js).
|
|
973
|
+
store.setStatus(status, "stream");
|
|
941
974
|
}
|
|
942
975
|
|
|
943
976
|
// Plugins' slow setup still running in the open session's worker (chi.init),
|
|
@@ -952,46 +985,52 @@ function onInitEvent(type, data) {
|
|
|
952
985
|
const done = applyInitEvent(initTasks, type, data);
|
|
953
986
|
renderInitRow();
|
|
954
987
|
if (!done) return;
|
|
955
|
-
const
|
|
988
|
+
const still = stillOn(openId);
|
|
956
989
|
setTimeout(() => {
|
|
957
|
-
if (
|
|
990
|
+
if (!still()) return;
|
|
958
991
|
fadeDone(initTasks, done);
|
|
959
992
|
renderInitRow();
|
|
960
993
|
setTimeout(() => {
|
|
961
|
-
if (
|
|
994
|
+
if (!still()) return;
|
|
962
995
|
dropDone(initTasks, done);
|
|
963
996
|
renderInitRow();
|
|
964
997
|
}, 400);
|
|
965
998
|
}, DONE_MS);
|
|
966
999
|
}
|
|
967
1000
|
|
|
968
|
-
// Forget
|
|
969
|
-
//
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
|
|
973
|
-
|
|
1001
|
+
// Forget everything the page knew about the open session: its fields (the
|
|
1002
|
+
// info bar, composer, command list), the turn on view, its stream and
|
|
1003
|
+
// question cards, its init row, the payload's markdown warning and tip and
|
|
1004
|
+
// capabilities, and the composer's Verify claim and Revise (+keepVerify+:
|
|
1005
|
+
// the same session opened again keeps them). The one reset list select() and
|
|
1006
|
+
// clearSelection() share, so a field the previous session set (its
|
|
1007
|
+
// "delegated by" parent, its commands, its card actions) never shows for
|
|
1008
|
+
// the next one, also when the next one's read fails.
|
|
1009
|
+
function forgetSession({ keepVerify = false } = {}) {
|
|
1010
|
+
if (!keepVerify) pendingVerifyAnchor = null;
|
|
1011
|
+
if (!keepVerify) setReviseAnchor(null);
|
|
1012
|
+
store.batch(() => {
|
|
1013
|
+
store.replaceCurrent(emptySessionState());
|
|
1014
|
+
featureGates.prime(store.current);
|
|
1015
|
+
});
|
|
974
1016
|
commandList.close();
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
function clearSelection() {
|
|
978
|
-
selected = null;
|
|
1017
|
+
resetTurnView();
|
|
979
1018
|
seedInitTasks(initTasks, []);
|
|
980
1019
|
renderInitRow();
|
|
981
|
-
setSessionHash(null);
|
|
982
|
-
lists.paintActive();
|
|
983
|
-
resetSessionState();
|
|
984
|
-
contextChips.clear();
|
|
985
|
-
participantsControl.load(null);
|
|
986
|
-
liveTurn.forget();
|
|
987
|
-
setTurnRunning(false);
|
|
988
|
-
closeStream();
|
|
989
1020
|
setMarkdownWarning(null);
|
|
990
1021
|
markdownTip.show(null);
|
|
991
1022
|
setCapabilities(null, null);
|
|
992
1023
|
composer.newVisit();
|
|
993
|
-
|
|
994
|
-
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function clearSelection() {
|
|
1027
|
+
store.batch(() => {
|
|
1028
|
+
store.update({ selected: null });
|
|
1029
|
+
setSessionHash(null);
|
|
1030
|
+
forgetSession();
|
|
1031
|
+
contextChips.clear();
|
|
1032
|
+
participantsControl.load(null);
|
|
1033
|
+
});
|
|
995
1034
|
focusPrompt();
|
|
996
1035
|
}
|
|
997
1036
|
|
|
@@ -1008,30 +1047,30 @@ function clearNotice() {
|
|
|
1008
1047
|
}
|
|
1009
1048
|
promptEl.addEventListener("input", clearNotice);
|
|
1010
1049
|
promptEl.addEventListener("input", () => noteDraft());
|
|
1011
|
-
// An emptied composer lets go of its Apply turn
|
|
1050
|
+
// An emptied composer lets go of its Apply turn, its Verify claim and its
|
|
1051
|
+
// Revise.
|
|
1012
1052
|
promptEl.addEventListener("input", () => {
|
|
1013
1053
|
if (promptEl.value.trim()) return;
|
|
1014
1054
|
deliveryControl.release();
|
|
1015
1055
|
pendingVerifyAnchor = null;
|
|
1056
|
+
setReviseAnchor(null);
|
|
1016
1057
|
});
|
|
1017
1058
|
|
|
1018
1059
|
async function select(id) {
|
|
1019
1060
|
clearNotice();
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
participantsControl.load(id);
|
|
1034
|
-
updateComposerMode();
|
|
1061
|
+
const same = store.selected === id;
|
|
1062
|
+
// Its synchronous part in one batch: the active card, the composer and
|
|
1063
|
+
// the bar redraw once, at its end (never across the await).
|
|
1064
|
+
store.batch(() => {
|
|
1065
|
+
store.update({ selected: id });
|
|
1066
|
+
setSessionHash(id);
|
|
1067
|
+
historyEl.replaceChildren(hintEl("Loading…"));
|
|
1068
|
+
emptyEl.classList.add("hidden");
|
|
1069
|
+
resyncAttempts = 0; // this session has its own re-read budget
|
|
1070
|
+
forgetSession({ keepVerify: same });
|
|
1071
|
+
contextChips.load(id);
|
|
1072
|
+
participantsControl.load(id);
|
|
1073
|
+
});
|
|
1035
1074
|
await resync();
|
|
1036
1075
|
// A folded composer stays folded: typing on the page opens it.
|
|
1037
1076
|
focusPrompt({ open: false });
|
|
@@ -1049,19 +1088,19 @@ function resetTurnView() {
|
|
|
1049
1088
|
}
|
|
1050
1089
|
|
|
1051
1090
|
// A re-read that failed (the phone still offline, chi web a moment): try it
|
|
1052
|
-
// again, backing off, and stop when the tries run out. The turn
|
|
1053
|
-
// alone meanwhile: a worker that still owns the turn keeps showing it running,
|
|
1091
|
+
// again, backing off, and stop when the tries run out. The running turn's DOM
|
|
1092
|
+
// is left alone meanwhile: a worker that still owns the turn keeps showing it running,
|
|
1054
1093
|
// and the hub's next event for the session is the fallback once the tries end.
|
|
1055
1094
|
const RESYNC_RETRIES = 3;
|
|
1056
1095
|
let resyncAttempts = 0;
|
|
1057
1096
|
|
|
1058
1097
|
function retryResync(id, { endText = null } = {}) {
|
|
1059
|
-
if (id !== selected) return;
|
|
1098
|
+
if (id !== store.selected) return;
|
|
1060
1099
|
const { attempt, delayMs } = resyncRetry({ attempts: resyncAttempts, max: RESYNC_RETRIES, baseMs: STREAM_RETRY_MS });
|
|
1061
1100
|
if (delayMs === null) return;
|
|
1062
1101
|
resyncAttempts = attempt;
|
|
1063
1102
|
setTimeout(() => {
|
|
1064
|
-
if (id !== selected || (streamControl && streamControl.live)) return;
|
|
1103
|
+
if (id !== store.selected || (streamControl && streamControl.live)) return;
|
|
1065
1104
|
resync({ endText });
|
|
1066
1105
|
}, delayMs);
|
|
1067
1106
|
}
|
|
@@ -1076,7 +1115,7 @@ function retryResync(id, { endText = null } = {}) {
|
|
|
1076
1115
|
// worker left unfinished; shown again after the re-render when no worker
|
|
1077
1116
|
// took over.
|
|
1078
1117
|
async function resync({ endText = null } = {}) {
|
|
1079
|
-
const id = selected;
|
|
1118
|
+
const id = store.selected;
|
|
1080
1119
|
if (!id) return;
|
|
1081
1120
|
resyncsInFlight += 1;
|
|
1082
1121
|
let data;
|
|
@@ -1085,14 +1124,14 @@ async function resync({ endText = null } = {}) {
|
|
|
1085
1124
|
data = await getSession(id, { parts: true });
|
|
1086
1125
|
} catch (e) {
|
|
1087
1126
|
if (e.status === 404) return leaveGoneSession(id, "not_found");
|
|
1088
|
-
if (id === selected) {
|
|
1127
|
+
if (id === store.selected) {
|
|
1089
1128
|
historyEl.replaceChildren(hintEl(`Error: ${e.message}`));
|
|
1090
1129
|
retryResync(id, { endText });
|
|
1091
1130
|
}
|
|
1092
1131
|
return;
|
|
1093
1132
|
}
|
|
1094
1133
|
resyncAttempts = 0;
|
|
1095
|
-
if (id !== selected) return;
|
|
1134
|
+
if (id !== store.selected) return;
|
|
1096
1135
|
// An archived session opens folded: decided once per visit, from its
|
|
1097
1136
|
// first answer (the list may not hold it yet, or ever under ?dir).
|
|
1098
1137
|
composer.decideVisit(data.session?.archived);
|
|
@@ -1102,14 +1141,14 @@ async function resync({ endText = null } = {}) {
|
|
|
1102
1141
|
showPendingClaim();
|
|
1103
1142
|
// Preview is read-only: a worker is woken only by POST /turn, so attach
|
|
1104
1143
|
// only when a live bridge exists.
|
|
1105
|
-
current
|
|
1106
|
-
if (current.streamSeq !== null) startStream(id, current.streamSeq);
|
|
1144
|
+
store.update({ current: { streamSeq: typeof data.last_event_seq === "number" ? (data.last_event_id || data.last_event_seq) : null } });
|
|
1145
|
+
if (store.current.streamSeq !== null) startStream(id, store.current.streamSeq);
|
|
1107
1146
|
else if (endText) addEndBubble(endText);
|
|
1108
1147
|
} finally {
|
|
1109
1148
|
resyncsInFlight -= 1;
|
|
1110
1149
|
if (resyncsInFlight === 0 && resyncAgain) {
|
|
1111
1150
|
resyncAgain = false;
|
|
1112
|
-
if (id === selected && !(streamControl && streamControl.live)) resync();
|
|
1151
|
+
if (id === store.selected && !(streamControl && streamControl.live)) resync();
|
|
1113
1152
|
}
|
|
1114
1153
|
}
|
|
1115
1154
|
}
|
|
@@ -1124,12 +1163,16 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1124
1163
|
// The saved fill (when there is one) goes in here too, so a reloaded or
|
|
1125
1164
|
// stopped session shows it before its next turn (a running turn's replay
|
|
1126
1165
|
// below streams over it).
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1166
|
+
const { status, ...fields } = sessionStateFrom(data);
|
|
1167
|
+
// The fields in one batch (their chips' popovers, the bar); the replay's
|
|
1168
|
+
// own redraws below stay where they run.
|
|
1169
|
+
store.batch(() => {
|
|
1170
|
+
store.update({ current: fields });
|
|
1171
|
+
store.setStatus(status, "payload");
|
|
1172
|
+
featureGates.prime(store.current);
|
|
1173
|
+
});
|
|
1174
|
+
liveTurn.timing.ended = store.current.timing.turnRecords.length;
|
|
1175
|
+
const activeTurnId = store.current.timing.activeTurn?.id || null;
|
|
1133
1176
|
if (data.messages && data.messages.length) renderHistory(data.messages);
|
|
1134
1177
|
else renderHistory(data.history || []);
|
|
1135
1178
|
seedWorkingSet(data);
|
|
@@ -1160,7 +1203,7 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1160
1203
|
current_turn: data.current_turn,
|
|
1161
1204
|
queued: data.queued,
|
|
1162
1205
|
queued_commands: data.queued_commands,
|
|
1163
|
-
started_at: current.timing.activeTurn?.started_at,
|
|
1206
|
+
started_at: store.current.timing.activeTurn?.started_at,
|
|
1164
1207
|
});
|
|
1165
1208
|
// Marked replayed: a call's duration isn't timed from the replay's clock.
|
|
1166
1209
|
replay.forEach((event) => streamHandlers[event.type]?.({ ...event, replayed: true }));
|
|
@@ -1168,8 +1211,8 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1168
1211
|
const gen = data.current_turn?.generation;
|
|
1169
1212
|
liveTurn.serverGen = gen ? { ...gen, at: Date.now() } : null;
|
|
1170
1213
|
if (!data.current_turn) {
|
|
1171
|
-
setTurnRunning(current.status === "running");
|
|
1172
|
-
if (liveTurn.running && current.timing.activeTurn?.started_at) liveTurn.timing.start(current.timing.activeTurn.started_at, activeTurnId);
|
|
1214
|
+
setTurnRunning(store.current.status === "running");
|
|
1215
|
+
if (liveTurn.running && store.current.timing.activeTurn?.started_at) liveTurn.timing.start(store.current.timing.activeTurn.started_at, activeTurnId);
|
|
1173
1216
|
} else if (liveTurn.running) {
|
|
1174
1217
|
// The replayed turn_started carries no id: the running turn's record's.
|
|
1175
1218
|
liveTurn.timing.turnId = activeTurnId;
|
|
@@ -1180,7 +1223,7 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1180
1223
|
// A question the worker is blocked on (also after a reload).
|
|
1181
1224
|
if (data.pending_question && data.pending_question.id && data.pending_question.status === "pending") {
|
|
1182
1225
|
// Its refs on the asking step, as question_requested puts them.
|
|
1183
|
-
if (data.pending_question.refs) turnDom.questionRefs
|
|
1226
|
+
if (data.pending_question.refs) turnDom.questionRefs(data.pending_question.refs);
|
|
1184
1227
|
questionCards.renderQuestion(data.pending_question);
|
|
1185
1228
|
// The turn's replay drew it as asked; the snapshot knows whether it is
|
|
1186
1229
|
// relayed to the parent now.
|
|
@@ -1188,8 +1231,12 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1188
1231
|
}
|
|
1189
1232
|
// A turn ran out of iterations and waits for a yes or no.
|
|
1190
1233
|
if (data.continue_offer) questionCards.renderContinue(data.continue_offer);
|
|
1191
|
-
|
|
1192
|
-
|
|
1234
|
+
// A whole view drawn: the composer and the bar once more (the working
|
|
1235
|
+
// set's seed may change the bar's chip).
|
|
1236
|
+
store.batch(() => {
|
|
1237
|
+
store.touch("composer");
|
|
1238
|
+
store.touch("infoBar");
|
|
1239
|
+
});
|
|
1193
1240
|
}
|
|
1194
1241
|
|
|
1195
1242
|
// The working set's turns from a session's messages, as turnHistoryNodes
|
|
@@ -1198,7 +1245,7 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1198
1245
|
function seedWorkingSet(data) {
|
|
1199
1246
|
if (!workingSet) return;
|
|
1200
1247
|
const items = data.messages || [];
|
|
1201
|
-
const groups = typeof items[0] === "object" && items[0]?.role ? turnGroups(items, current.timing).filter((g) => g.kind === "turn") : [];
|
|
1248
|
+
const groups = typeof items[0] === "object" && items[0]?.role ? turnGroups(items, store.current.timing).filter((g) => g.kind === "turn") : [];
|
|
1202
1249
|
if (data.current_turn && groups.length) groups.pop();
|
|
1203
1250
|
workingSet.seed(groups.map((g, i) => {
|
|
1204
1251
|
const text = String(items[g.user]?.content ?? "").replace(/\s+/g, " ").trim();
|
|
@@ -1213,10 +1260,10 @@ function renderHistory(items) {
|
|
|
1213
1260
|
return;
|
|
1214
1261
|
}
|
|
1215
1262
|
if (typeof items[0] === "object" && items[0].role) {
|
|
1216
|
-
historyEl.replaceChildren(turnHistoryNodes(items, current.timing, { thumbs: (images) => thumbsTemplate(selected, images), workingSet: !!workingSet }));
|
|
1263
|
+
historyEl.replaceChildren(turnHistoryNodes(items, store.current.timing, { thumbs: (images) => thumbsTemplate(store.selected, images), workingSet: !!workingSet }));
|
|
1217
1264
|
renderSavedVerdicts();
|
|
1218
1265
|
// The live dividers count from the history's last activity.
|
|
1219
|
-
gapClock.seed(lastActivity(turnGroups(items, current.timing), items));
|
|
1266
|
+
gapClock.seed(lastActivity(turnGroups(items, store.current.timing), items));
|
|
1220
1267
|
linkTextRefs();
|
|
1221
1268
|
} else {
|
|
1222
1269
|
historyEl.replaceChildren(...items.map((c) => elementOf(outputBubbleTemplate(c))));
|
|
@@ -1253,33 +1300,31 @@ installRefClicks(document.body, pageViewer, {
|
|
|
1253
1300
|
});
|
|
1254
1301
|
|
|
1255
1302
|
async function readTurnEnd() {
|
|
1256
|
-
if (!selected) return;
|
|
1257
|
-
const id = selected;
|
|
1303
|
+
if (!store.selected) return;
|
|
1304
|
+
const id = store.selected;
|
|
1258
1305
|
// The line and the turn this read is for, before the await: a turn
|
|
1259
1306
|
// queued behind this one may start (a new live line) before it lands.
|
|
1260
1307
|
const line = liveTurn.timing.capture();
|
|
1308
|
+
const still = stillOn(openId);
|
|
1261
1309
|
try {
|
|
1262
1310
|
const d = await getSession(id, { tail: true, recent: true, turnId: line.turnId });
|
|
1263
|
-
if (
|
|
1311
|
+
if (!still()) return;
|
|
1264
1312
|
if (d.session) {
|
|
1265
|
-
if (Array.isArray(d.session.used_memory_names)) current.usedMemories = d.session.used_memory_names;
|
|
1266
|
-
// The notes chip: the first turn's build (or a /model) records them.
|
|
1267
|
-
if (Array.isArray(d.session.prompt_notes)) current.promptNotes = d.session.prompt_notes;
|
|
1268
|
-
if (d.session.status) current.status = d.session.status;
|
|
1269
1313
|
if (!(await mergeTurnTiming(id, d.timing))) return;
|
|
1270
1314
|
liveTurn.timing.finishLine(line);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1315
|
+
// The rest in one batch: the bar and the lists redraw once, after the
|
|
1316
|
+
// answer's markdown.
|
|
1317
|
+
store.batch(() => {
|
|
1318
|
+
// The notes chip: the first turn's build (or a /model) records them.
|
|
1319
|
+
applySessionPayload(d, { fields: ["usedMemories", "promptNotes"] });
|
|
1320
|
+
turnEndStatus(d);
|
|
1321
|
+
setMarkdownWarning(d.markdown_warning);
|
|
1322
|
+
markdownTip.show(d.markdown_tip);
|
|
1323
|
+
setCapabilities(d.capabilities, d.editor_url);
|
|
1324
|
+
if (Array.isArray(d.messages)) applyFinalMarkdown(d.messages);
|
|
1325
|
+
// Its card, replaced (replaceSelectedCard).
|
|
1326
|
+
replaceSelectedCard((sess) => ({ ...sess, used_memory_names: store.current.usedMemories.slice(), status: store.current.status }));
|
|
1327
|
+
});
|
|
1283
1328
|
}
|
|
1284
1329
|
} catch (_) {
|
|
1285
1330
|
// A failed read (the server or worker gone a moment) leaves the answer
|
|
@@ -1297,7 +1342,7 @@ function seedSeenFromDom() {
|
|
|
1297
1342
|
// delta), so the gap measured is the one the user was sitting at. While the
|
|
1298
1343
|
// stage holds a turn its rows go there: nothing it does moves the history.
|
|
1299
1344
|
function isNearBottom() {
|
|
1300
|
-
return !turnDom.holds
|
|
1345
|
+
return !turnDom.holds() && atHistoryEnd();
|
|
1301
1346
|
}
|
|
1302
1347
|
|
|
1303
1348
|
function atHistoryEnd() {
|
|
@@ -1314,7 +1359,7 @@ function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false
|
|
|
1314
1359
|
// gone mid-turn is re-rendered (resync), which counts again.
|
|
1315
1360
|
if (kind !== "gone") liveTurn.timing.ended += 1;
|
|
1316
1361
|
// A completed turn with no answer: its notice where the answer goes
|
|
1317
|
-
// (the turn
|
|
1362
|
+
// (the turn block places it), else after the turn; before the timing.
|
|
1318
1363
|
const endNote = emptyAnswer ? elementOf(emptyAnswerTemplate(emptyAnswer)) : null;
|
|
1319
1364
|
const kept = turnDom.turnEnded({ kind, endNote, emptyAnswer: !!emptyAnswer, turnId: liveTurn.timing.turnId }) || [];
|
|
1320
1365
|
if (endNote && !endNote.isConnected) appendToHistory(endNote);
|
|
@@ -1322,7 +1367,7 @@ function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false
|
|
|
1322
1367
|
if (endText) addEndBubble(endText, { stopped });
|
|
1323
1368
|
if (kept.length) {
|
|
1324
1369
|
kept.forEach((el) => appendToHistory(el));
|
|
1325
|
-
if (!turnDom.holds
|
|
1370
|
+
if (!turnDom.holds()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1326
1371
|
}
|
|
1327
1372
|
// The question dies with its turn (the worker stopped waiting).
|
|
1328
1373
|
if (questionCards.pendingQuestion()) questionCards.resolveQuestion(questionCards.pendingQuestion().id, { cancelled: true, reason: "turn ended" });
|
|
@@ -1339,17 +1384,16 @@ infoTextEl.addEventListener("click", (e) => {
|
|
|
1339
1384
|
}
|
|
1340
1385
|
});
|
|
1341
1386
|
infoTextEl.addEventListener("click", async (e) => {
|
|
1342
|
-
if (!e.target.closest(".copy-attach") || !selected) return;
|
|
1343
|
-
const id = selected;
|
|
1344
|
-
if (!(await copyText(`chi --attach ${id}`)) || id !== selected) return;
|
|
1345
|
-
current
|
|
1346
|
-
|
|
1347
|
-
setTimeout(updateInfoBar, 2000);
|
|
1387
|
+
if (!e.target.closest(".copy-attach") || !store.selected) return;
|
|
1388
|
+
const id = store.selected;
|
|
1389
|
+
if (!(await copyText(`chi --attach ${id}`)) || id !== store.selected) return;
|
|
1390
|
+
store.update({ current: { attachCopiedAt: Date.now() } });
|
|
1391
|
+
setTimeout(() => store.touch("infoBar"), 2000);
|
|
1348
1392
|
});
|
|
1349
1393
|
|
|
1350
1394
|
// Apply (review_apply.js) under a participant's review turn that just
|
|
1351
1395
|
// ended live ({turn_id, author, author_name}, from its turn_started), once:
|
|
1352
|
-
// a reload draws it from the history (
|
|
1396
|
+
// a reload draws it from the history (turn_block.js).
|
|
1353
1397
|
function placeApply(data) {
|
|
1354
1398
|
const badge = authorBadge(data.author, data.author_name);
|
|
1355
1399
|
if (!badge || historyEl.querySelector(`.review-apply[data-apply-turn="${CSS.escape(data.turn_id)}"]`)) return;
|
|
@@ -1358,10 +1402,12 @@ function placeApply(data) {
|
|
|
1358
1402
|
|
|
1359
1403
|
function placeVerdict(record) {
|
|
1360
1404
|
if (!record) return;
|
|
1361
|
-
const known = (current.verdicts || []).some((item) => item.check_turn_id === record.check_turn_id);
|
|
1362
|
-
|
|
1405
|
+
const known = (store.current.verdicts || []).some((item) => item.check_turn_id === record.check_turn_id);
|
|
1406
|
+
const patch = {};
|
|
1407
|
+
if (!known) patch.verdicts = [...(store.current.verdicts || []), record];
|
|
1363
1408
|
// A fresh verdict ends its check's "verifying fresh" (the card shows it).
|
|
1364
|
-
if (record.fresh)
|
|
1409
|
+
if (record.fresh) patch.verifyFresh = settleRunning(store.current.verifyFresh, record);
|
|
1410
|
+
store.update({ current: patch });
|
|
1365
1411
|
const badge = record.by && authorBadge(record.by);
|
|
1366
1412
|
placeVerdictWithFresh(historyEl, record, {
|
|
1367
1413
|
who: badge ? badge.name : "You",
|
|
@@ -1369,7 +1415,7 @@ function placeVerdict(record) {
|
|
|
1369
1415
|
roots: CLAIM_ROOTS,
|
|
1370
1416
|
opts: claimOpts(),
|
|
1371
1417
|
onMissingCheck: (missing) => {
|
|
1372
|
-
const hash = ownerCheckHash(
|
|
1418
|
+
const hash = ownerCheckHash(store.sessions, store.selected, missing.check_turn_id);
|
|
1373
1419
|
if (hash) location.hash = hash;
|
|
1374
1420
|
else showToast(CHECK_NOT_ON_PAGE);
|
|
1375
1421
|
},
|
|
@@ -1380,12 +1426,11 @@ function placeVerdict(record) {
|
|
|
1380
1426
|
// always theirs to start; its running check links to the child's session.
|
|
1381
1427
|
const verifyFreshErrors = {};
|
|
1382
1428
|
function verifyFreshState() {
|
|
1383
|
-
const id = selected;
|
|
1384
|
-
const unavailable = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
1385
|
-
if (!Array.isArray(current.verifyFresh)) current.verifyFresh = [];
|
|
1429
|
+
const id = store.selected;
|
|
1430
|
+
const unavailable = verifyWorkerUnavailable(store.current.workerFeatures, store.current.bridgeUp);
|
|
1386
1431
|
return {
|
|
1387
|
-
verdicts: current.verdicts || [],
|
|
1388
|
-
running: current.verifyFresh,
|
|
1432
|
+
verdicts: store.current.verdicts || [],
|
|
1433
|
+
running: store.current.verifyFresh || [],
|
|
1389
1434
|
errors: verifyFreshErrors,
|
|
1390
1435
|
canStart: !!id && !unavailable,
|
|
1391
1436
|
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
@@ -1402,19 +1447,19 @@ function verifyFreshState() {
|
|
|
1402
1447
|
}
|
|
1403
1448
|
|
|
1404
1449
|
function renderSavedVerdicts() {
|
|
1405
|
-
for (const record of current.verdicts || []) placeVerdict(record);
|
|
1450
|
+
for (const record of store.current.verdicts || []) placeVerdict(record);
|
|
1406
1451
|
}
|
|
1407
1452
|
|
|
1408
1453
|
// Apply: the owner's Apply turn into the composer, after what is typed
|
|
1409
1454
|
// there; never sent from here.
|
|
1410
1455
|
historyEl.addEventListener("click", async (e) => {
|
|
1411
1456
|
const button = e.target.closest(".review-apply-btn");
|
|
1412
|
-
if (!button || !selected) return;
|
|
1413
|
-
const id = selected;
|
|
1457
|
+
if (!button || !store.selected) return;
|
|
1458
|
+
const id = store.selected;
|
|
1414
1459
|
button.disabled = true;
|
|
1415
1460
|
try {
|
|
1416
1461
|
const text = await fetchApply(id, button.dataset.applyTurn);
|
|
1417
|
-
if (id === selected) {
|
|
1462
|
+
if (id === store.selected) {
|
|
1418
1463
|
fillComposer(applyComposerText(promptEl.value, text));
|
|
1419
1464
|
// The owner's own turn: queued after a running one, never steered in.
|
|
1420
1465
|
deliveryControl.hold();
|
|
@@ -1428,18 +1473,18 @@ historyEl.addEventListener("click", async (e) => {
|
|
|
1428
1473
|
|
|
1429
1474
|
// The open session's worker badge (info_bar_model.js workerBadgeFor).
|
|
1430
1475
|
function currentWorkerBadge() {
|
|
1431
|
-
return workerBadgeFor(selected, current, versions);
|
|
1476
|
+
return workerBadgeFor(store.selected, store.current, versions);
|
|
1432
1477
|
}
|
|
1433
1478
|
|
|
1434
1479
|
// Restart: the session moves to a new worker on the newest chi; the page
|
|
1435
1480
|
// follows it (its bridge_up), the composer's draft stays as it is.
|
|
1436
1481
|
async function restartWorker() {
|
|
1437
1482
|
const badge = currentWorkerBadge();
|
|
1438
|
-
if (!badge || !badge.restart || restartingIds.has(selected)) return;
|
|
1439
|
-
if (!confirm(restartConfirmText(current.workerVersion, badge.newest))) return;
|
|
1440
|
-
const id = selected;
|
|
1483
|
+
if (!badge || !badge.restart || restartingIds.has(store.selected)) return;
|
|
1484
|
+
if (!confirm(restartConfirmText(store.current.workerVersion, badge.newest))) return;
|
|
1485
|
+
const id = store.selected;
|
|
1441
1486
|
restartingIds.add(id);
|
|
1442
|
-
|
|
1487
|
+
store.touch("infoBar");
|
|
1443
1488
|
try {
|
|
1444
1489
|
const result = await restartSession(id);
|
|
1445
1490
|
showToast(result.version ? `Restarted on chi ${result.version}` : "Restarting: the new worker is starting");
|
|
@@ -1447,7 +1492,7 @@ async function restartWorker() {
|
|
|
1447
1492
|
showToast(cantText("restart", e), null, { sticky: true });
|
|
1448
1493
|
} finally {
|
|
1449
1494
|
restartingIds.delete(id);
|
|
1450
|
-
|
|
1495
|
+
store.touch("infoBar");
|
|
1451
1496
|
}
|
|
1452
1497
|
}
|
|
1453
1498
|
|
|
@@ -1456,7 +1501,7 @@ async function restartWorker() {
|
|
|
1456
1501
|
function updateInfoBar() {
|
|
1457
1502
|
annotateBar?.refresh();
|
|
1458
1503
|
const { buttons, bar } = sessionBar({
|
|
1459
|
-
id: selected, state: current, sessions:
|
|
1504
|
+
id: store.selected, state: store.current, sessions: store.sessions, versions, cardTitle: (s) => lists.cardTitle(s),
|
|
1460
1505
|
continuing: sessionActions.continuing, restarting: restartingIds, streaming: !!streamControl, running: liveTurn.running,
|
|
1461
1506
|
now: Date.now(), workingSet: workingSet ? workingSet.chip() : undefined, participants: participantsControl.chip(),
|
|
1462
1507
|
});
|
|
@@ -1494,9 +1539,9 @@ function updateInfoBar() {
|
|
|
1494
1539
|
// browser tab names it too. It follows the card's (openTitle): a chain link
|
|
1495
1540
|
// reads its card's title (cardTitle), whatever opener it was sent.
|
|
1496
1541
|
function shownTitle() {
|
|
1497
|
-
const shown = selected ? lists.sessionsById().get(selected) : null;
|
|
1498
|
-
const first = current.firstPreview || "";
|
|
1499
|
-
const raw = !selected ? "" : openTitle({ shown, first, cardTitle: (s) => lists.cardTitle(s) });
|
|
1542
|
+
const shown = store.selected ? lists.sessionsById().get(store.selected) : null;
|
|
1543
|
+
const first = store.current.firstPreview || "";
|
|
1544
|
+
const raw = !store.selected ? "" : openTitle({ shown, first, cardTitle: (s) => lists.cardTitle(s) });
|
|
1500
1545
|
return raw === "\u2014" ? "" : raw; // "—": no first message yet
|
|
1501
1546
|
}
|
|
1502
1547
|
// The top bar's ✎ follows it (not a plain terminal's session), and an open
|
|
@@ -1511,7 +1556,7 @@ function updateSessionTitle() {
|
|
|
1511
1556
|
});
|
|
1512
1557
|
if (sessionTitleEl.textContent !== text) sessionTitleEl.textContent = text;
|
|
1513
1558
|
if (sessionTitleRowEl.hidden !== !text) sessionTitleRowEl.hidden = !text;
|
|
1514
|
-
const renamable = !!text && renameShown(current.owner);
|
|
1559
|
+
const renamable = !!text && renameShown(store.current.owner);
|
|
1515
1560
|
if (titleRenameBtn.hidden !== !renamable) titleRenameBtn.hidden = !renamable;
|
|
1516
1561
|
const tip = text ? title : "";
|
|
1517
1562
|
if (sessionTitleEl.title !== tip) sessionTitleEl.title = tip;
|
|
@@ -1520,7 +1565,7 @@ function updateSessionTitle() {
|
|
|
1520
1565
|
tabShown = tab;
|
|
1521
1566
|
notifications.setBaseTitle(tab);
|
|
1522
1567
|
}
|
|
1523
|
-
rename.follow(selected);
|
|
1568
|
+
rename.follow(store.selected);
|
|
1524
1569
|
rename.reposition();
|
|
1525
1570
|
}
|
|
1526
1571
|
|
|
@@ -1528,7 +1573,7 @@ function updateSessionTitle() {
|
|
|
1528
1573
|
// @return true when the stage took +el+ (it scrolls it into its view itself)
|
|
1529
1574
|
function appendToHistory(el) {
|
|
1530
1575
|
// (The stage's place first moves a turn still sliding away into the history.)
|
|
1531
|
-
if (turnDom.place
|
|
1576
|
+
if (turnDom.place(el)) return true;
|
|
1532
1577
|
appendAboveLiveTiming(historyEl, el, liveTurn.timing.el);
|
|
1533
1578
|
return false;
|
|
1534
1579
|
}
|
|
@@ -1539,31 +1584,61 @@ function appendToHistory(el) {
|
|
|
1539
1584
|
// turn that started meanwhile keeps its line and its own active turn.
|
|
1540
1585
|
// @return false when the session changed meanwhile
|
|
1541
1586
|
async function mergeTurnTiming(id, data) {
|
|
1542
|
-
const keepActive = () => (liveTurn.running ? current.timing.activeTurn : null);
|
|
1543
|
-
const merged = mergeTiming(current.timing, data);
|
|
1544
|
-
|
|
1587
|
+
const keepActive = () => (liveTurn.running ? store.current.timing.activeTurn : null);
|
|
1588
|
+
const merged = mergeTiming(store.current.timing, data);
|
|
1589
|
+
store.update({ current: { timing: { ...merged.timing, activeTurn: keepActive() } } });
|
|
1545
1590
|
if (merged.short) {
|
|
1591
|
+
const still = stillOn(openId, id);
|
|
1546
1592
|
try {
|
|
1547
1593
|
const full = await getSession(id, { timing: true });
|
|
1548
|
-
if (
|
|
1549
|
-
|
|
1594
|
+
if (!still()) return false;
|
|
1595
|
+
store.update({ current: { timing: { ...normalizeTiming(full.timing), activeTurn: keepActive() } } });
|
|
1550
1596
|
} catch (_) {
|
|
1551
|
-
if (
|
|
1597
|
+
if (!still()) return false;
|
|
1552
1598
|
}
|
|
1553
1599
|
}
|
|
1554
|
-
liveTurn.timing.ended = Math.max(liveTurn.timing.ended, current.timing.turnRecords.length);
|
|
1600
|
+
liveTurn.timing.ended = Math.max(liveTurn.timing.ended, store.current.timing.turnRecords.length);
|
|
1555
1601
|
return true;
|
|
1556
1602
|
}
|
|
1557
1603
|
|
|
1558
1604
|
function setUsedMemories(names) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1605
|
+
store.batch(() => {
|
|
1606
|
+
store.update({ current: { usedMemories: names.slice() } });
|
|
1607
|
+
replaceSelectedCard((sess) => ({ ...sess, used_memory_names: store.current.usedMemories.slice() }));
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// The open session's card replaced by +change(card)+'s copy, in a new list
|
|
1612
|
+
// (replaceCard). @return whether the list has it
|
|
1613
|
+
function replaceSelectedCard(change) {
|
|
1614
|
+
return replaceCard(store.selected, change);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Session +id+'s card replaced by +change(card)+'s copy, in a new list
|
|
1618
|
+
// (never changed in place: setStatus and the hub's events replace too).
|
|
1619
|
+
// @return whether the list has it
|
|
1620
|
+
function replaceCard(id, change) {
|
|
1621
|
+
const i = store.sessions.findIndex((s) => s.id === id);
|
|
1622
|
+
if (i < 0) return false;
|
|
1623
|
+
const next = store.sessions.slice();
|
|
1624
|
+
next[i] = change(next[i]);
|
|
1625
|
+
store.update({ sessions: next });
|
|
1626
|
+
return true;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// A partial re-read of the open session (readTurnEnd, the turn_canceled
|
|
1630
|
+
// timing re-read): its named +fields+ into the store, read as a full load
|
|
1631
|
+
// reads them (session_payload.js). Not its status: turnEndStatus. Their
|
|
1632
|
+
// redraws follow (session_fields.js FIELDS).
|
|
1633
|
+
function applySessionPayload(data, { fields }) {
|
|
1634
|
+
store.update({ current: sessionFieldsFrom(data, fields) });
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// A turn end's re-read's status, when it carries one (the "turnEnd" rule:
|
|
1638
|
+
// not over a turn that started meanwhile).
|
|
1639
|
+
function turnEndStatus(data) {
|
|
1640
|
+
const { status } = sessionFieldsFrom(data, ["status"]);
|
|
1641
|
+
if (status) store.setStatus(status, "turnEnd", { running: liveTurn.running });
|
|
1567
1642
|
}
|
|
1568
1643
|
|
|
1569
1644
|
// A turn or command just woke the session's worker: follow its stream if
|
|
@@ -1572,12 +1647,12 @@ function setUsedMemories(names) {
|
|
|
1572
1647
|
// worker have been up already, turn_started says so (replaysDrawnTurn).
|
|
1573
1648
|
function followWokenWorker() {
|
|
1574
1649
|
if (streamControl?.live) return;
|
|
1575
|
-
startStream(selected, current.streamSeq ?? 0);
|
|
1576
|
-
current
|
|
1650
|
+
startStream(store.selected, store.current.streamSeq ?? 0);
|
|
1651
|
+
store.update({ current: { streamFromGuess: store.current.streamSeq === null } });
|
|
1577
1652
|
}
|
|
1578
1653
|
|
|
1579
1654
|
function startStream(id, fromSeq) {
|
|
1580
|
-
current
|
|
1655
|
+
store.update({ current: { streamFromGuess: false } });
|
|
1581
1656
|
if (streamControl) {
|
|
1582
1657
|
streamControl.close();
|
|
1583
1658
|
streamControl = null;
|
|
@@ -1589,7 +1664,7 @@ function startStream(id, fromSeq) {
|
|
|
1589
1664
|
onStreamClosed: () => workerGone(id),
|
|
1590
1665
|
onStreamError: () => workerGone(id, { opened: false }),
|
|
1591
1666
|
});
|
|
1592
|
-
|
|
1667
|
+
store.touch("infoBar"); // streaming
|
|
1593
1668
|
}
|
|
1594
1669
|
|
|
1595
1670
|
// +opened+ false: the stream never delivered a frame (the proxy answered
|
|
@@ -1598,38 +1673,33 @@ function startStream(id, fromSeq) {
|
|
|
1598
1673
|
const STREAM_RETRY_MS = 3000;
|
|
1599
1674
|
function workerGone(id, { opened = true } = {}) {
|
|
1600
1675
|
streamControl = null;
|
|
1601
|
-
if (id !== selected) return;
|
|
1676
|
+
if (id !== store.selected) return;
|
|
1602
1677
|
// A turn that was running is over only when the worker really left: a
|
|
1603
1678
|
// stream that merely dropped (no stop, it was open) waits for the re-read.
|
|
1604
1679
|
const stopped = sessionActions.stoppingId() === id;
|
|
1605
1680
|
const { endNow, endText } = streamDropEnd({ turnRunning: liveTurn.running, stopped, opened });
|
|
1606
|
-
if (endNow) {
|
|
1607
|
-
|
|
1608
|
-
if (!stopped) setLiveStatus("idle");
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (i >= 0 && allSessions[i].owner === "worker") {
|
|
1617
|
-
allSessions[i] = withoutWorker(allSessions[i]);
|
|
1618
|
-
lists.render();
|
|
1619
|
-
}
|
|
1620
|
-
updateInfoBar();
|
|
1681
|
+
if (endNow) endTurnView("gone", endText, { stopped });
|
|
1682
|
+
store.batch(() => {
|
|
1683
|
+
if (endNow && !stopped) setLiveStatus("idle");
|
|
1684
|
+
store.update({ current: { ...(store.current.owner === "worker" ? { owner: null } : {}), bridgeUp: false } });
|
|
1685
|
+
// Its card says "live" (the list's owner, or setLiveStatus's) until the
|
|
1686
|
+
// hub's event says otherwise: the worker is gone now, as far as this
|
|
1687
|
+
// tab can tell.
|
|
1688
|
+
if (store.sessions.find((s) => s.id === id)?.owner === "worker") replaceCard(id, withoutWorker);
|
|
1689
|
+
store.touch("infoBar"); // streaming
|
|
1690
|
+
});
|
|
1621
1691
|
// Once: a worker still alive with a bridge (the stream dropped, not the
|
|
1622
1692
|
// worker: chi web restarted) reconnects right here. Otherwise the next
|
|
1623
1693
|
// worker's bridge_up event brings the stream back (followSelectedSession).
|
|
1624
1694
|
if (opened) resync({ endText });
|
|
1625
|
-
else setTimeout(() => { if (id === selected && !(streamControl && streamControl.live)) resync({ endText }); }, STREAM_RETRY_MS);
|
|
1695
|
+
else setTimeout(() => { if (id === store.selected && !(streamControl && streamControl.live)) resync({ endText }); }, STREAM_RETRY_MS);
|
|
1626
1696
|
}
|
|
1627
1697
|
|
|
1628
1698
|
// The stream's event handlers (stream_handlers.js); renderSessionView
|
|
1629
1699
|
// replays a snapshot through them too.
|
|
1630
1700
|
const streamHandlers = createStreamHandlers({
|
|
1631
|
-
selected: () => selected,
|
|
1632
|
-
|
|
1701
|
+
selected: () => store.selected,
|
|
1702
|
+
store,
|
|
1633
1703
|
clientId,
|
|
1634
1704
|
capabilities: () => pageCapabilities,
|
|
1635
1705
|
historyEl,
|
|
@@ -1638,20 +1708,18 @@ const streamHandlers = createStreamHandlers({
|
|
|
1638
1708
|
shownTexts,
|
|
1639
1709
|
questionCards,
|
|
1640
1710
|
contextChips,
|
|
1641
|
-
llmContextControl,
|
|
1642
|
-
thinkingControl,
|
|
1643
|
-
reasoningHistoryControl,
|
|
1644
1711
|
workingSet,
|
|
1645
1712
|
participantsControl,
|
|
1646
1713
|
placeApply,
|
|
1647
1714
|
placeVerdict,
|
|
1648
1715
|
restoreVerify: (anchor) => { pendingVerifyAnchor = anchor; },
|
|
1716
|
+
restoreRevise,
|
|
1649
1717
|
sentIds,
|
|
1650
1718
|
earlyRestores,
|
|
1651
1719
|
stream: {
|
|
1652
1720
|
takeGuess: () => {
|
|
1653
|
-
const guess = current.streamFromGuess;
|
|
1654
|
-
current
|
|
1721
|
+
const guess = store.current.streamFromGuess;
|
|
1722
|
+
store.update({ current: { streamFromGuess: false } });
|
|
1655
1723
|
return guess;
|
|
1656
1724
|
},
|
|
1657
1725
|
restart: () => {
|
|
@@ -1674,7 +1742,7 @@ const streamHandlers = createStreamHandlers({
|
|
|
1674
1742
|
getSession,
|
|
1675
1743
|
// The page's hooks, passed as they are: function declarations (hoisted)
|
|
1676
1744
|
// and the views' methods bound above.
|
|
1677
|
-
resync,
|
|
1745
|
+
resync, setTurnRunning, setLiveStatus, endTurnView, readTurnEnd, refreshTimingFromSession,
|
|
1678
1746
|
setUsedMemories, onInitEvent, findInTurnRoots, applyFinalMarkdown,
|
|
1679
1747
|
// bubbles.js
|
|
1680
1748
|
applyPromptOps, showSteers, markRecapStale, appendChunk, makeUserBubble, addEndBubble, commandBubble,
|
|
@@ -1686,14 +1754,16 @@ const streamHandlers = createStreamHandlers({
|
|
|
1686
1754
|
});
|
|
1687
1755
|
|
|
1688
1756
|
async function refreshTimingFromSession() {
|
|
1689
|
-
if (!selected) return;
|
|
1690
|
-
const id = selected;
|
|
1757
|
+
if (!store.selected) return;
|
|
1758
|
+
const id = store.selected;
|
|
1691
1759
|
const line = liveTurn.timing.capture();
|
|
1760
|
+
const still = stillOn(openId);
|
|
1692
1761
|
try {
|
|
1693
1762
|
const data = await getSession(id, { tail: true, recent: true, turnId: line.turnId });
|
|
1694
|
-
if (
|
|
1695
|
-
|
|
1696
|
-
if (await mergeTurnTiming(id, data.timing))
|
|
1763
|
+
if (!still()) return;
|
|
1764
|
+
turnEndStatus(data);
|
|
1765
|
+
if (!(await mergeTurnTiming(id, data.timing))) return;
|
|
1766
|
+
liveTurn.timing.finishLine(line);
|
|
1697
1767
|
} catch (_) {}
|
|
1698
1768
|
}
|
|
1699
1769
|
|
|
@@ -1710,14 +1780,13 @@ function closeStream() {
|
|
|
1710
1780
|
// put it (they follow, with its owner and bridge_up).
|
|
1711
1781
|
function addCreatedSession(created) {
|
|
1712
1782
|
const { bridge_port: _port, ...session } = created;
|
|
1713
|
-
|
|
1714
|
-
lists.render();
|
|
1783
|
+
store.update({ sessions: applySessionEvent(store.sessions, "session", { session }) });
|
|
1715
1784
|
}
|
|
1716
1785
|
|
|
1717
1786
|
// /model, !ls … from the composer, and a control's command (the llm ctx
|
|
1718
1787
|
// and thinking chips): composer.js.
|
|
1719
1788
|
const composerCommands = createComposerCommands({
|
|
1720
|
-
session: () => selected,
|
|
1789
|
+
session: () => store.selected,
|
|
1721
1790
|
clientId,
|
|
1722
1791
|
sendCommand,
|
|
1723
1792
|
followWokenWorker: () => followWokenWorker(),
|
|
@@ -1726,15 +1795,15 @@ const composerCommands = createComposerCommands({
|
|
|
1726
1795
|
// As send_turn.js says a turn's failure after a switch.
|
|
1727
1796
|
failedElsewhere: (line, text, id) => showToast(cantText(`run ${line} in ${id.slice(0, 8)}`, text), null, { sticky: true }),
|
|
1728
1797
|
ownedByTui: () => {
|
|
1729
|
-
|
|
1730
|
-
|
|
1798
|
+
// The composer and the bar (its continue and stop, the title's ✎) follow.
|
|
1799
|
+
store.update({ current: { owner: "tui" } });
|
|
1731
1800
|
},
|
|
1732
1801
|
composer: {
|
|
1733
1802
|
busy: () => { actionBtn.disabled = true; },
|
|
1734
1803
|
text: () => promptEl.value,
|
|
1735
1804
|
clear: () => clearComposer(),
|
|
1736
1805
|
done: () => {
|
|
1737
|
-
actionBtn.disabled = current.owner === "tui";
|
|
1806
|
+
actionBtn.disabled = store.current.owner === "tui";
|
|
1738
1807
|
// A chip's command (the info bar) leaves a folded composer folded.
|
|
1739
1808
|
focusPrompt({ open: false });
|
|
1740
1809
|
},
|
|
@@ -1744,8 +1813,8 @@ const composerCommands = createComposerCommands({
|
|
|
1744
1813
|
// The composer's send (send_turn.js): a line into the open session, or a
|
|
1745
1814
|
// new session first on the start page.
|
|
1746
1815
|
const sendTurnControl = createSendTurn({
|
|
1747
|
-
session: () => selected,
|
|
1748
|
-
|
|
1816
|
+
session: () => store.selected,
|
|
1817
|
+
store,
|
|
1749
1818
|
running: () => liveTurn.running,
|
|
1750
1819
|
clientId,
|
|
1751
1820
|
sentIds,
|
|
@@ -1754,6 +1823,10 @@ const sendTurnControl = createSendTurn({
|
|
|
1754
1823
|
verify: () => pendingVerifyAnchor,
|
|
1755
1824
|
clearVerify: (anchor) => { if (pendingVerifyAnchor === anchor) pendingVerifyAnchor = null; },
|
|
1756
1825
|
restoreVerify: (anchor) => { pendingVerifyAnchor = anchor; },
|
|
1826
|
+
// Not while the gate refuses (the chip says so; the line goes as a plain message).
|
|
1827
|
+
revise: () => (reviseUnavailable() ? null : pendingReviseAnchor),
|
|
1828
|
+
clearRevise: (anchor) => { if (pendingReviseAnchor === anchor) setReviseAnchor(null); },
|
|
1829
|
+
restoreRevise,
|
|
1757
1830
|
dir: scope.dir,
|
|
1758
1831
|
startPage,
|
|
1759
1832
|
takeDelivery: () => deliveryControl.take(),
|
|
@@ -1776,8 +1849,6 @@ const sendTurnControl = createSendTurn({
|
|
|
1776
1849
|
addCreatedSession,
|
|
1777
1850
|
select,
|
|
1778
1851
|
showNotice,
|
|
1779
|
-
updateInfoBar,
|
|
1780
|
-
updateComposerMode,
|
|
1781
1852
|
followWokenWorker,
|
|
1782
1853
|
refreshHistory,
|
|
1783
1854
|
leaveGoneSession,
|
|
@@ -1803,18 +1874,18 @@ function clearComposer() {
|
|
|
1803
1874
|
// A queue (Alt+Enter, or an Apply turn's Enter) to a worker that can't
|
|
1804
1875
|
// queue (it would steer): not sent, the text kept, a note says why.
|
|
1805
1876
|
function queueRefused(altKey) {
|
|
1806
|
-
if (!selected || !liveTurn.running || current.workerFeatures.includes("delivery")) return false;
|
|
1877
|
+
if (!store.selected || !liveTurn.running || store.current.workerFeatures.includes("delivery")) return false;
|
|
1807
1878
|
if (!altKey && !deliveryControl.held()) return false;
|
|
1808
1879
|
addCommandBubble({ label: null, line: promptEl.value.trim(), text: QUEUE_NEEDS_WORKER, failed: true });
|
|
1809
1880
|
return true;
|
|
1810
1881
|
}
|
|
1811
1882
|
|
|
1812
1883
|
async function handleCancel() {
|
|
1813
|
-
if (!selected) return;
|
|
1884
|
+
if (!store.selected) return;
|
|
1814
1885
|
cancelBtn.disabled = true;
|
|
1815
1886
|
infoCancelBtn.disabled = true;
|
|
1816
1887
|
try {
|
|
1817
|
-
await cancelTurn(selected);
|
|
1888
|
+
await cancelTurn(store.selected);
|
|
1818
1889
|
} catch (e) {
|
|
1819
1890
|
// The turn ended meanwhile: nothing left to cancel.
|
|
1820
1891
|
if (e.code !== "not_running") alert(e.message);
|
|
@@ -1858,7 +1929,7 @@ const composer = mountComposer({
|
|
|
1858
1929
|
// Alt/Option+Enter: queued for after the running turn (D6), only to a
|
|
1859
1930
|
// worker that queues (else it would steer): the text stays, a note says why.
|
|
1860
1931
|
send: ({ queue }) => {
|
|
1861
|
-
if (!selected) return handleSendTurn();
|
|
1932
|
+
if (!store.selected) return handleSendTurn();
|
|
1862
1933
|
if (!queueRefused(queue)) handleSendTurn(queue ? { delivery: "queue" } : {});
|
|
1863
1934
|
},
|
|
1864
1935
|
keyFirst: (e) => commandList.handleKey(e),
|
|
@@ -1867,8 +1938,8 @@ const composer = mountComposer({
|
|
|
1867
1938
|
fold: {
|
|
1868
1939
|
infoCancelBtn,
|
|
1869
1940
|
infoBarEl: $("#infoBar"),
|
|
1870
|
-
session: () => selected,
|
|
1871
|
-
waiting: () => !!(waitingOn(
|
|
1941
|
+
session: () => store.selected,
|
|
1942
|
+
waiting: () => !!(waitingOn(store.sessions.find((s) => s.id === store.selected)) || questionCards.pendingQuestion()),
|
|
1872
1943
|
turnRunning: () => liveTurn.running,
|
|
1873
1944
|
allViewOpen,
|
|
1874
1945
|
lightboxOpen: () => !lightboxEl.hidden,
|
|
@@ -1889,7 +1960,7 @@ function refreshHistory() {
|
|
|
1889
1960
|
|
|
1890
1961
|
// The × on a message queued for after the turn takes it back (unqueue.js).
|
|
1891
1962
|
const unqueue = createUnqueue({
|
|
1892
|
-
sessionId: () => selected,
|
|
1963
|
+
sessionId: () => store.selected,
|
|
1893
1964
|
clientId,
|
|
1894
1965
|
toast: (text, action, options) => showToast(text, action, options),
|
|
1895
1966
|
copy: (text) => copyText(text),
|
|
@@ -1923,7 +1994,7 @@ infoCancelBtn.addEventListener("click", () => handleCancel());
|
|
|
1923
1994
|
const commandList = createCommandList({
|
|
1924
1995
|
list: $("#commandList"),
|
|
1925
1996
|
prompt: promptEl,
|
|
1926
|
-
commands: () => (selected && current.owner !== "tui" ? current.commands : []),
|
|
1997
|
+
commands: () => (store.selected && store.current.owner !== "tui" ? store.current.commands : []),
|
|
1927
1998
|
afterPick: () => {
|
|
1928
1999
|
noteDraft();
|
|
1929
2000
|
fitPrompt();
|
|
@@ -1932,7 +2003,7 @@ const commandList = createCommandList({
|
|
|
1932
2003
|
|
|
1933
2004
|
$("#infoStopBtn").addEventListener("click", () => sessionActions.stopSelected());
|
|
1934
2005
|
$("#infoDeleteBtn").addEventListener("click", () => {
|
|
1935
|
-
if (selected) confirmDelete(selected);
|
|
2006
|
+
if (store.selected) confirmDelete(store.selected);
|
|
1936
2007
|
});
|
|
1937
2008
|
$("#infoContinueBtn").addEventListener("click", () => continueSelected());
|
|
1938
2009
|
$("#infoArchiveBtn").addEventListener("click", () => sessionActions.toggleArchiveSelected());
|
|
@@ -1995,16 +2066,16 @@ function fillComposer(text) {
|
|
|
1995
2066
|
annotateBar = createAnnotateBar({
|
|
1996
2067
|
presets: withVerify(parsePresets(document.body.dataset.annotatePresets)),
|
|
1997
2068
|
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
1998
|
-
&& verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp) ? VERIFY_UNAVAILABLE : null,
|
|
2069
|
+
&& verifyWorkerUnavailable(store.current.workerFeatures, store.current.bridgeUp) ? VERIFY_UNAVAILABLE : null,
|
|
1999
2070
|
offeredPreset: (preset, source, range) => preset !== VERIFY_PRESET || !!verifyAnchorFor({ source, range }),
|
|
2000
|
-
canAnnotate: () => !!selected && current.owner !== "tui",
|
|
2071
|
+
canAnnotate: () => !!store.selected && store.current.owner !== "tui",
|
|
2001
2072
|
roots: turnRoots,
|
|
2002
2073
|
isLive: (root) => liveTurn.running && turnDom.containsLive(root),
|
|
2003
2074
|
onQuote: (text) => fillComposer(appendQuote(promptEl.value, text)),
|
|
2004
2075
|
onPreset: (preset, fields) => {
|
|
2005
2076
|
pendingVerifyAnchor = preset === VERIFY_PRESET ? verifyAnchorFor(fields) : null;
|
|
2006
2077
|
},
|
|
2007
|
-
actions: () => annotationActions(current.commands),
|
|
2078
|
+
actions: () => annotationActions(store.current.commands),
|
|
2008
2079
|
running: () => liveTurn.running,
|
|
2009
2080
|
onAction: (action, fields) => {
|
|
2010
2081
|
const line = actionLine(action, fields);
|
|
@@ -2033,13 +2104,29 @@ historyEl.addEventListener("click", (e) => {
|
|
|
2033
2104
|
const button = e.target.closest?.("[data-confirm-line]");
|
|
2034
2105
|
if (button) applyConfirm(button, runSessionCommand);
|
|
2035
2106
|
});
|
|
2036
|
-
turnDom.scrollEl
|
|
2107
|
+
turnDom.scrollEl.addEventListener("scroll", annotateBar.schedule, true);
|
|
2037
2108
|
turnRoots().forEach((root) => installCopy(root));
|
|
2038
2109
|
// Long answers fold by hand (answer_fold.js), per viewer and session.
|
|
2039
2110
|
installAnswerFold({
|
|
2040
2111
|
historyEl,
|
|
2041
|
-
sessionId: () => selected,
|
|
2042
|
-
projectRoot: () =>
|
|
2112
|
+
sessionId: () => store.selected,
|
|
2113
|
+
projectRoot: () => store.sessions.find((s) => s.id === store.selected)?.project_root ?? null,
|
|
2114
|
+
});
|
|
2115
|
+
// Revise stacks (revise_stack.js): the superseded versions fold to stubs,
|
|
2116
|
+
// the top answer gets its version chip, a final answer its Revise button.
|
|
2117
|
+
reviseStacks = installReviseStacks({
|
|
2118
|
+
historyEl,
|
|
2119
|
+
sessionId: () => store.selected,
|
|
2120
|
+
projectRoot: () => store.sessions.find((s) => s.id === store.selected)?.project_root ?? null,
|
|
2121
|
+
running: () => (liveTurn.running ? { turnId: liveTurn.timing.turnId || null } : null),
|
|
2122
|
+
verifyTurn: (turnId) => (store.current.verdicts || []).some((v) => v.check_turn_id === turnId),
|
|
2123
|
+
unavailable: reviseUnavailable,
|
|
2124
|
+
armed: () => pendingReviseAnchor?.turn_id ?? null,
|
|
2125
|
+
onRevise: (anchor) => {
|
|
2126
|
+
setReviseAnchor(anchor);
|
|
2127
|
+
openForVisit();
|
|
2128
|
+
focusPrompt();
|
|
2129
|
+
},
|
|
2043
2130
|
});
|
|
2044
2131
|
|
|
2045
2132
|
// Notifications (notifications.js): an OS notification and a title count
|
|
@@ -2100,6 +2187,24 @@ document.addEventListener("keydown", (e) => {
|
|
|
2100
2187
|
}
|
|
2101
2188
|
closeAllView();
|
|
2102
2189
|
});
|
|
2190
|
+
// The redraws the store runs, by name (session_store.js ORDER), after a
|
|
2191
|
+
// write of what they read (session_fields.js FIELDS; sessions, selected) or
|
|
2192
|
+
// a touch(). Registered here, once every piece they call exists: a write
|
|
2193
|
+
// before it (none at load) would redraw nothing.
|
|
2194
|
+
store.on("lists", () => lists.render());
|
|
2195
|
+
store.on("paintActive", () => lists.paintActive());
|
|
2196
|
+
store.on("llmContextChip", () => llmContextControl.refresh());
|
|
2197
|
+
store.on("thinkingChip", () => thinkingControl.refresh());
|
|
2198
|
+
store.on("reasoningChip", () => reasoningHistoryControl.refresh());
|
|
2199
|
+
// The working set's list offers pin only under forget.
|
|
2200
|
+
store.on("workingSetStrategy", () => workingSet?.strategyChanged());
|
|
2201
|
+
store.on("featureGates", () => featureGates.check(store.current));
|
|
2202
|
+
store.on("composer", updateComposerMode);
|
|
2203
|
+
// The answers' Revise buttons follow liveTurn.running too (none on the
|
|
2204
|
+
// running turn's answer), which touches the composer.
|
|
2205
|
+
store.on("composer", () => reviseStacks.decorate());
|
|
2206
|
+
store.on("infoBar", updateInfoBar);
|
|
2207
|
+
|
|
2103
2208
|
applyView();
|
|
2104
2209
|
|
|
2105
2210
|
// Where sessions live: under the all-sessions search (the server fills it in).
|
|
@@ -2109,8 +2214,10 @@ if (document.body.dataset.sessionsDir) {
|
|
|
2109
2214
|
}
|
|
2110
2215
|
|
|
2111
2216
|
renderScope();
|
|
2112
|
-
|
|
2113
|
-
|
|
2217
|
+
store.batch(() => {
|
|
2218
|
+
store.touch("composer");
|
|
2219
|
+
store.touch("infoBar");
|
|
2220
|
+
});
|
|
2114
2221
|
startPage.loadModels();
|
|
2115
2222
|
// The list: the hub's stream when the browser has EventSource, else fetched.
|
|
2116
2223
|
if (typeof globalThis.EventSource === "function") openSessionEvents();
|