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