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