samagotchi 0.134.0 → 0.136.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -1
- data/bin/chi +2 -3
- data/docs/architecture.md +6 -3
- data/docs/cli.md +43 -38
- data/docs/configuration.md +11 -9
- data/docs/guardrails.md +53 -9
- data/docs/internals/engine.md +6 -3
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/web-frontend.md +263 -0
- data/docs/internals/working-set.md +2 -2
- data/docs/sessions.md +5 -3
- data/docs/testing.md +36 -6
- data/lib/samagotchi/bridge.rb +11 -6
- data/lib/samagotchi/bridge_client.rb +8 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +0 -2
- data/lib/samagotchi/engine.rb +69 -12
- data/lib/samagotchi/guardrail_wiring.rb +8 -6
- data/lib/samagotchi/guardrails/approval.rb +41 -5
- data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
- data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
- data/lib/samagotchi/guardrails/pair_command.rb +387 -78
- data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
- data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
- data/lib/samagotchi/guardrails/shared_git.rb +1 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
- data/lib/samagotchi/guardrails_command.rb +13 -10
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/message_parts.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +33 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager.rb +13 -11
- data/lib/samagotchi/system_one/approval_question.rb +21 -16
- data/lib/samagotchi/system_one/self_protect.rb +13 -15
- data/lib/samagotchi/tools/delegate_relay.rb +6 -5
- data/lib/samagotchi/tools/delegate_wait.rb +5 -5
- data/lib/samagotchi/tools/execute.rb +6 -6
- data/lib/samagotchi/tools/output_guardrails.rb +9 -2
- data/lib/samagotchi/tools/read.rb +5 -5
- data/lib/samagotchi/turn_anchors.rb +49 -0
- data/lib/samagotchi/verify_anchor.rb +5 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -18
- data/lib/samagotchi/web/message_display.rb +33 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_view.rb +8 -0
- data/lib/samagotchi/web/public/activity.js +1 -1
- data/lib/samagotchi/web/public/annotations.js +1 -1
- data/lib/samagotchi/web/public/answer_fold.js +100 -31
- data/lib/samagotchi/web/public/app.js +451 -367
- data/lib/samagotchi/web/public/bubbles.js +30 -10
- data/lib/samagotchi/web/public/cards_place.js +4 -2
- data/lib/samagotchi/web/public/chip_popover.js +2 -2
- data/lib/samagotchi/web/public/composer.css +8 -0
- data/lib/samagotchi/web/public/context_chips.js +9 -6
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/dock_reserve.js +13 -15
- data/lib/samagotchi/web/public/feature_gates.js +33 -0
- data/lib/samagotchi/web/public/history_reveal.js +10 -6
- data/lib/samagotchi/web/public/index.html +105 -53
- data/lib/samagotchi/web/public/info_bar.js +11 -12
- data/lib/samagotchi/web/public/info_bar_model.js +8 -10
- data/lib/samagotchi/web/public/latest.js +46 -0
- data/lib/samagotchi/web/public/level_chip.js +33 -95
- data/lib/samagotchi/web/public/lists_view.js +15 -13
- data/lib/samagotchi/web/public/live_timing.js +2 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/method_api.js +14 -0
- data/lib/samagotchi/web/public/notifications.js +4 -11
- data/lib/samagotchi/web/public/participant.js +11 -6
- data/lib/samagotchi/web/public/participant_names.js +1 -1
- data/lib/samagotchi/web/public/participants_panel.js +4 -2
- data/lib/samagotchi/web/public/project_picker.js +18 -22
- data/lib/samagotchi/web/public/question_cards.js +2 -2
- data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
- data/lib/samagotchi/web/public/restore.js +2 -0
- data/lib/samagotchi/web/public/revise_stack.js +378 -0
- data/lib/samagotchi/web/public/scope.js +12 -20
- data/lib/samagotchi/web/public/send_turn.js +29 -16
- data/lib/samagotchi/web/public/session_actions.js +15 -14
- data/lib/samagotchi/web/public/session_fields.js +152 -0
- data/lib/samagotchi/web/public/session_payload.js +35 -0
- data/lib/samagotchi/web/public/session_state.js +9 -5
- data/lib/samagotchi/web/public/session_store.js +191 -0
- data/lib/samagotchi/web/public/settings_chip.js +212 -0
- data/lib/samagotchi/web/public/settings_panel.js +121 -0
- data/lib/samagotchi/web/public/shell_command.js +26 -0
- data/lib/samagotchi/web/public/stage_model.js +4 -4
- data/lib/samagotchi/web/public/stage_view.js +11 -16
- data/lib/samagotchi/web/public/start_settings.js +25 -88
- data/lib/samagotchi/web/public/stream_handlers.js +88 -90
- data/lib/samagotchi/web/public/task_stop.js +1 -1
- data/lib/samagotchi/web/public/thinking_chip.js +7 -45
- data/lib/samagotchi/web/public/time_gaps.js +5 -2
- data/lib/samagotchi/web/public/timing.js +15 -5
- data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
- data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +33 -93
- data/lib/samagotchi/web/public/turn_events.js +3 -3
- data/lib/samagotchi/web/public/turn_html.js +20 -16
- data/lib/samagotchi/web/public/turn_model.js +6 -6
- data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +3 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/web/session_control.rb +2 -2
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +6 -3
- data/lib/samagotchi/worker.rb +9 -9
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +15 -3
- data/lib/samagotchi/web/public/hold.js +0 -78
|
@@ -7,13 +7,16 @@ import { inTree } from "./sessions_list.js";
|
|
|
7
7
|
// and Unarchive, Delete and Stop, from the info bar's buttons, a card's or a
|
|
8
8
|
// batch; and the start page when the open session is gone.
|
|
9
9
|
//
|
|
10
|
-
// Hooks: +selected()+ the open session's id or null; +
|
|
11
|
-
// (
|
|
10
|
+
// Hooks: +selected()+ the open session's id or null; +store+ the page's
|
|
11
|
+
// session store (session_store.js: Stop's status; touch("infoBar") for the
|
|
12
|
+
// continue and review in-flight sets, which it doesn't hold); +sessions()+ /
|
|
13
|
+
// +setSessions(list)+ the page's list (the store's: a new list redraws the
|
|
14
|
+
// lists and the bar);
|
|
12
15
|
// +api+: {continueSession, startReview, archiveSession, unarchiveSession,
|
|
13
16
|
// deleteSession, stopSession, sessionHolds, stopSessionHolds} (data.js);
|
|
14
17
|
// +eventsLive()+ the hub's stream is open (else
|
|
15
|
-
// +refresh()+ fetches the list);
|
|
16
|
-
// +select(id)+, +clearSelection()+, +showNotice(text)+,
|
|
18
|
+
// +refresh()+ fetches the list);
|
|
19
|
+
// +select(id)+, +clearSelection()+, +showNotice(text)+,
|
|
17
20
|
// +toast(text, action, options)+, +addCreatedSession(created)+,
|
|
18
21
|
// +fillOpener(created, continuedId)+ (the new link's composer; returns a draft
|
|
19
22
|
// from another session it displaced, or null), +restoreDraft(draft, opener)+
|
|
@@ -28,8 +31,8 @@ export function reviewStartedText(id, created) {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function createSessionActions({
|
|
31
|
-
selected,
|
|
32
|
-
select, clearSelection, showNotice,
|
|
34
|
+
selected, store, sessions, setSessions, api, eventsLive, refresh,
|
|
35
|
+
select, clearSelection, showNotice, toast, addCreatedSession, fillOpener, restoreDraft, closeLiveStream, workerGone,
|
|
33
36
|
alert = (text) => globalThis.alert(text), confirm = (text) => globalThis.confirm(text),
|
|
34
37
|
// The Stop sheet (stop_sheet.js): +open(id, holds)+ shows it and answers
|
|
35
38
|
// true; nothing to stop (or no sheet wired) is false, and Stop is the
|
|
@@ -59,7 +62,6 @@ export function createSessionActions({
|
|
|
59
62
|
}
|
|
60
63
|
setSessions(sessions().filter((s) => s.id !== id));
|
|
61
64
|
if (selected() === id) clearSelection();
|
|
62
|
-
lists.render();
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// The open session no longer exists: back to the start page, saying why.
|
|
@@ -146,7 +148,7 @@ export function createSessionActions({
|
|
|
146
148
|
const id = selected();
|
|
147
149
|
const short = id.slice(0, 8);
|
|
148
150
|
continuingIds.add(id);
|
|
149
|
-
|
|
151
|
+
store.touch("infoBar");
|
|
150
152
|
try {
|
|
151
153
|
const created = await api.continueSession(id);
|
|
152
154
|
markArchived([id], true);
|
|
@@ -176,7 +178,7 @@ export function createSessionActions({
|
|
|
176
178
|
}
|
|
177
179
|
} finally {
|
|
178
180
|
continuingIds.delete(id);
|
|
179
|
-
|
|
181
|
+
store.touch("infoBar");
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
@@ -192,7 +194,7 @@ export function createSessionActions({
|
|
|
192
194
|
if (!selected() || reviewingIds.has(selected())) return null;
|
|
193
195
|
const id = selected();
|
|
194
196
|
reviewingIds.add(id);
|
|
195
|
-
|
|
197
|
+
store.touch("infoBar");
|
|
196
198
|
try {
|
|
197
199
|
const created = await api.startReview(id);
|
|
198
200
|
addCreatedSession(created);
|
|
@@ -210,7 +212,7 @@ export function createSessionActions({
|
|
|
210
212
|
return null;
|
|
211
213
|
} finally {
|
|
212
214
|
reviewingIds.delete(id);
|
|
213
|
-
|
|
215
|
+
store.touch("infoBar");
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
|
|
@@ -235,7 +237,7 @@ export function createSessionActions({
|
|
|
235
237
|
// archive): don't wait for its stream to notice.
|
|
236
238
|
function stoppedWorker(id) {
|
|
237
239
|
closeLiveStream();
|
|
238
|
-
|
|
240
|
+
store.setStatus("stopped", "stop");
|
|
239
241
|
workerGone(id);
|
|
240
242
|
}
|
|
241
243
|
|
|
@@ -270,8 +272,7 @@ export function createSessionActions({
|
|
|
270
272
|
alert(`Could not ${archive ? "archive" : "unarchive"} session ${short}: ${answer.error.message}`);
|
|
271
273
|
return;
|
|
272
274
|
}
|
|
273
|
-
lists.
|
|
274
|
-
updateInfoBar();
|
|
275
|
+
// markArchived's list redrew the lists and the bar already.
|
|
275
276
|
if (!eventsLive()) refresh();
|
|
276
277
|
const what = archive ? "Archived" : "Unarchived";
|
|
277
278
|
toast(`${what} ${short}`, undo ? { label: "Undo", run: () => setArchived(id, !archive, { undo: false }) } : null);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The open session's fields (session_state.js emptySessionState), one entry
|
|
2
|
+
// each: where a payload (GET /sessions/:id) and a hub card carry it, what
|
|
3
|
+
// redraws it (session_store.js ORDER names) and who writes it. The table a
|
|
4
|
+
// new field or a new writer starts from (docs/internals/web-frontend.md,
|
|
5
|
+
// "The session store").
|
|
6
|
+
//
|
|
7
|
+
// Every write goes through the store (app.js `store`: update, setStatus,
|
|
8
|
+
// replaceCurrent), never `current.X = …` (the lint rule in
|
|
9
|
+
// eslint.config.js). Two writers stay outside, on purpose (D8):
|
|
10
|
+
// - live_timing.js sets `timing().activeTurn` in place (start, finish,
|
|
11
|
+
// finishLine): the running turn's own clock, which mergeTurnTiming keeps.
|
|
12
|
+
// - verdict_fresh.js `startFresh` pushes onto `fresh.running`, which is
|
|
13
|
+
// `verifyFresh` (verifyFreshState hands the array out): returning the
|
|
14
|
+
// entry for an update would leave the card holding the old array.
|
|
15
|
+
//
|
|
16
|
+
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
17
|
+
// new export of an old module would leave a cached copy without it.
|
|
18
|
+
|
|
19
|
+
import { withLiveStatus } from "./format.js";
|
|
20
|
+
|
|
21
|
+
const isList = (v) => Array.isArray(v);
|
|
22
|
+
const list = (v) => (isList(v) ? v : undefined);
|
|
23
|
+
|
|
24
|
+
// Per field:
|
|
25
|
+
// source(payload) the payload carries it (not undefined): a partial
|
|
26
|
+
// re-read takes it (session_payload.js sessionFieldsFrom).
|
|
27
|
+
// No source: a payload always sets it (sessionStateFrom),
|
|
28
|
+
// or never does (the page's own fields).
|
|
29
|
+
// card(sess) its value on a hub card (followSelectedSession)
|
|
30
|
+
// redraws what reads it (session_store.js ORDER)
|
|
31
|
+
// writers who writes it (besides renderSessionView's payload and
|
|
32
|
+
// forgetSession's reset, which write every field)
|
|
33
|
+
export const FIELDS = {
|
|
34
|
+
firstPreview: { redraws: ["infoBar"], writers: "send_turn.js (the first prompt)" },
|
|
35
|
+
usedMemories: {
|
|
36
|
+
source: (d) => list(d.session?.used_memory_names),
|
|
37
|
+
redraws: ["infoBar"],
|
|
38
|
+
writers: "setUsedMemories (stream), readTurnEnd; both replace the card too",
|
|
39
|
+
},
|
|
40
|
+
preloadedMemories: { source: (d) => list(d.session?.preloaded_memory_names), redraws: ["infoBar"], writers: "the payload" },
|
|
41
|
+
mutedMemories: { source: (d) => list(d.session?.muted_memory_names), redraws: ["infoBar"], writers: "the payload" },
|
|
42
|
+
promptNotes: { source: (d) => list(d.session?.prompt_notes), redraws: ["infoBar"], writers: "readTurnEnd" },
|
|
43
|
+
parentId: { redraws: ["infoBar"], writers: "the payload" },
|
|
44
|
+
continues: { redraws: ["infoBar"], writers: "the payload" },
|
|
45
|
+
review: { redraws: ["infoBar"], writers: "the payload" },
|
|
46
|
+
verdicts: {
|
|
47
|
+
source: (d) => list(d.session?.verdicts),
|
|
48
|
+
redraws: [],
|
|
49
|
+
writers: "placeVerdict (its card draws itself: placeVerdictWithFresh)",
|
|
50
|
+
},
|
|
51
|
+
verifyFresh: {
|
|
52
|
+
source: (d) => list(d.session?.verify_fresh),
|
|
53
|
+
redraws: [],
|
|
54
|
+
writers: "placeVerdict (settleRunning); verdict_fresh.js startFresh pushes in place (D8)",
|
|
55
|
+
},
|
|
56
|
+
createdAt: { redraws: ["infoBar"], writers: "the payload" },
|
|
57
|
+
ctxPct: { redraws: ["infoBar"], writers: "stream_handlers.js (generation_chunk, context_status)" },
|
|
58
|
+
ctxWindow: { redraws: ["infoBar"], writers: "stream_handlers.js (generation_started, context_status)" },
|
|
59
|
+
ctxWindowSource: { redraws: ["infoBar"], writers: "stream_handlers.js (generation_started, context_status)" },
|
|
60
|
+
ctxUnknownAfterSwitch: { redraws: ["infoBar"], writers: "stream_handlers.js (generation_started, context_status)" },
|
|
61
|
+
status: {
|
|
62
|
+
source: (d) => d.session?.status || undefined,
|
|
63
|
+
card: (sess) => sess.status,
|
|
64
|
+
redraws: ["infoBar"],
|
|
65
|
+
writers: "store.setStatus only (statusWrite below): the hub, the stream, a turn end's re-read, Stop, the payload",
|
|
66
|
+
},
|
|
67
|
+
thinking: { redraws: ["settingsPanel", "infoBar"], writers: "stream_handlers.js command_ran" },
|
|
68
|
+
model: {
|
|
69
|
+
source: (d) => d.session?.model_name,
|
|
70
|
+
redraws: ["infoBar"],
|
|
71
|
+
writers: "stream_handlers.js command_ran (a new model also syncModelPick: an event, D9)",
|
|
72
|
+
},
|
|
73
|
+
served: { redraws: ["infoBar"], writers: "stream_handlers.js (command_ran, generation_completed)" },
|
|
74
|
+
dir: { source: (d) => d.session?.working_directory, redraws: ["infoBar"], writers: "the payload" },
|
|
75
|
+
owner: {
|
|
76
|
+
card: (sess) => sess.owner || null,
|
|
77
|
+
redraws: ["composer", "infoBar"],
|
|
78
|
+
writers: "followSelectedSession, workerGone (worker → null), send_turn.js and composerCommands.ownedByTui (\"tui\")",
|
|
79
|
+
},
|
|
80
|
+
commands: {
|
|
81
|
+
source: (d) => list(d.commands),
|
|
82
|
+
redraws: ["infoBar"],
|
|
83
|
+
writers: "the payload (the / list and the annotate bar read it at use; the bar refreshes the annotate bar)",
|
|
84
|
+
},
|
|
85
|
+
workerVersion: { card: (sess) => sess.worker_version || null, redraws: ["infoBar"], writers: "followSelectedSession" },
|
|
86
|
+
workerFeatures: {
|
|
87
|
+
card: (sess) => (isList(sess.worker_features) ? sess.worker_features : []),
|
|
88
|
+
redraws: ["featureGates", "infoBar"],
|
|
89
|
+
writers: "followSelectedSession",
|
|
90
|
+
},
|
|
91
|
+
bridgeUp: {
|
|
92
|
+
card: (sess) => sess.bridge_up === true,
|
|
93
|
+
redraws: ["featureGates", "infoBar"],
|
|
94
|
+
writers: "followSelectedSession, workerGone",
|
|
95
|
+
},
|
|
96
|
+
llmContext: {
|
|
97
|
+
redraws: ["settingsPanel", "workingSetStrategy", "infoBar"],
|
|
98
|
+
writers: "stream_handlers.js command_ran",
|
|
99
|
+
},
|
|
100
|
+
reasoningHistory: { redraws: ["settingsPanel", "infoBar"], writers: "stream_handlers.js command_ran" },
|
|
101
|
+
toolkit: { redraws: ["infoBar"], writers: "stream_handlers.js (command_ran, hook_notice)" },
|
|
102
|
+
toolkitDefault: { redraws: ["infoBar"], writers: "the payload" },
|
|
103
|
+
timing: {
|
|
104
|
+
source: (d) => d.timing,
|
|
105
|
+
redraws: ["infoBar"],
|
|
106
|
+
writers: "mergeTurnTiming; stream_handlers.js (generation_completed: tokens, modelSegments; side_usage: side), " +
|
|
107
|
+
"always a new object; live_timing.js sets activeTurn in place (D8)",
|
|
108
|
+
},
|
|
109
|
+
turnStarted: { redraws: ["composer", "infoBar"], writers: "setTurnRunning" },
|
|
110
|
+
streamSeq: { redraws: [], writers: "resync" },
|
|
111
|
+
streamFromGuess: { redraws: [], writers: "followWokenWorker, startStream, the stream's takeGuess" },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// The fields a hub card brings for the open session (followSelectedSession):
|
|
115
|
+
// its owner and worker. Never its status: that goes through setStatus, so
|
|
116
|
+
// "the hub's status only while not streaming" can't be bypassed.
|
|
117
|
+
const CARD_FIELDS = ["owner", "workerVersion", "workerFeatures", "bridgeUp"];
|
|
118
|
+
|
|
119
|
+
// @param sess a hub card (the open session's)
|
|
120
|
+
// @return {owner, workerVersion, workerFeatures, bridgeUp}
|
|
121
|
+
export function fieldsFromCard(sess) {
|
|
122
|
+
return Object.fromEntries(CARD_FIELDS.map((key) => [key, FIELDS[key].card(sess)]));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Whether a status write from +source+ applies, and what it does to the
|
|
126
|
+
// open session's card (null: the card is left alone).
|
|
127
|
+
// payload renderSessionView: always
|
|
128
|
+
// hub followSelectedSession: only while the stream isn't live
|
|
129
|
+
// stream setLiveStatus (turn_*, workerGone's idle): always; the card
|
|
130
|
+
// takes it too (withLiveStatus: status, owner, updated_at)
|
|
131
|
+
// turnEnd readTurnEnd, the turn_canceled re-read: only while no turn
|
|
132
|
+
// runs (a re-read landing after the next queued turn's
|
|
133
|
+
// turn_started doesn't write its stored "idle" over "running";
|
|
134
|
+
// mergeTurnTiming keeps activeTurn by the same test); the caller
|
|
135
|
+
// skips a payload without one; readTurnEnd writes its card
|
|
136
|
+
// itself, after its timing merge
|
|
137
|
+
// stop session_actions.js stoppedWorker: always; workerGone's
|
|
138
|
+
// withoutWorker takes care of the card
|
|
139
|
+
// @param source one of the above
|
|
140
|
+
// @param streaming whether the open session's stream is live
|
|
141
|
+
// @param running whether a turn runs on the page (liveTurn.running)
|
|
142
|
+
// @return {apply, card: (sess, status) => sess | null}
|
|
143
|
+
export function statusWrite(source, { streaming = false, running = false } = {}) {
|
|
144
|
+
switch (source) {
|
|
145
|
+
case "payload": return { apply: true, card: null };
|
|
146
|
+
case "hub": return { apply: !streaming, card: null };
|
|
147
|
+
case "stream": return { apply: true, card: withLiveStatus };
|
|
148
|
+
case "turnEnd": return { apply: !running, card: null };
|
|
149
|
+
case "stop": return { apply: true, card: null };
|
|
150
|
+
default: throw new Error(`statusWrite: no source ${source}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// A partial re-read of the open session (GET /sessions/:id with ?tail,
|
|
2
|
+
// ?recent…): the fields of the page's session state it brings, read as a
|
|
3
|
+
// full load reads them (session_state.js sessionStateFrom), so a field's
|
|
4
|
+
// rule lives in one place. app.js applySessionPayload writes them through
|
|
5
|
+
// the store; the caller then calls the field's redraw
|
|
6
|
+
// (docs/internals/web-frontend.md, "The session store").
|
|
7
|
+
//
|
|
8
|
+
// A new module, not an export of session_state.js: the page's modules are
|
|
9
|
+
// cached at unversioned URLs, and a new export of an old module would
|
|
10
|
+
// leave a cached copy without it.
|
|
11
|
+
|
|
12
|
+
import { sessionStateFrom } from "./session_state.js";
|
|
13
|
+
import { FIELDS } from "./session_fields.js";
|
|
14
|
+
|
|
15
|
+
// Where each field comes from in the payload (session_fields.js FIELDS
|
|
16
|
+
// sources): a field whose source the payload doesn't carry (a partial read
|
|
17
|
+
// leaves it out, or an older chi web never sent it) is not touched. Fields
|
|
18
|
+
// without one are always taken.
|
|
19
|
+
//
|
|
20
|
+
// The named +fields+ of the session state +data+ sets (sessionStateFrom's
|
|
21
|
+
// values), those whose source it carries.
|
|
22
|
+
// @param data a GET /sessions/:id answer (it has a session)
|
|
23
|
+
// @param fields the state's keys (emptySessionState's), e.g. ["status"]
|
|
24
|
+
// @return {field: value} for the fields it brings
|
|
25
|
+
export function sessionFieldsFrom(data, fields) {
|
|
26
|
+
if (!data?.session) return {};
|
|
27
|
+
const all = sessionStateFrom(data);
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const field of fields) {
|
|
30
|
+
const source = FIELDS[field]?.source;
|
|
31
|
+
if (source && source(data) === undefined) continue;
|
|
32
|
+
if (field in all) out[field] = all[field];
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
@@ -40,6 +40,8 @@ export function emptySessionState() {
|
|
|
40
40
|
// (stream_handlers.js context_status): the ctx tooltip says it waits.
|
|
41
41
|
ctxUnknownAfterSwitch: false,
|
|
42
42
|
status: "",
|
|
43
|
+
// The level the next turn thinks at ({level, source, own}: the thinking chip), or null.
|
|
44
|
+
thinking: null,
|
|
43
45
|
model: "",
|
|
44
46
|
// What the worker's server said it served for a model name: [served, asked].
|
|
45
47
|
served: null,
|
|
@@ -55,12 +57,15 @@ export function emptySessionState() {
|
|
|
55
57
|
workerFeatures: [],
|
|
56
58
|
bridgeUp: false,
|
|
57
59
|
// The LLM context strategy, apply rule and budget the next turn runs
|
|
58
|
-
// under, each with its source (the info bar's
|
|
60
|
+
// under, each with its source (the info bar's ⚙ chip), or null.
|
|
59
61
|
llmContext: null,
|
|
60
62
|
// Which of its reasoning the model gets back from the next turn on, with
|
|
61
|
-
// its source (the
|
|
63
|
+
// its source (the ⚙ chip's), or null (a native host).
|
|
62
64
|
reasoningHistory: null,
|
|
63
65
|
toolkit: null,
|
|
66
|
+
// config's toolkit.default as chi web reads it (the 🧰 chip: a session on
|
|
67
|
+
// it shows 🧰 alone), or null (an older chi web: no 🧰 for full).
|
|
68
|
+
toolkitDefault: null,
|
|
64
69
|
timing: normalizeTiming(),
|
|
65
70
|
// A turn started on it while the page had it open (setTurnRunning): a
|
|
66
71
|
// fresh link's picker goes at once, before the timing re-read brings the
|
|
@@ -74,8 +79,6 @@ export function emptySessionState() {
|
|
|
74
79
|
// The stream was opened from 0 (a wake with no cursor to go on) and no
|
|
75
80
|
// new turn has started on it yet (app.js followWokenWorker).
|
|
76
81
|
streamFromGuess: false,
|
|
77
|
-
// When the info bar's "copy chi --attach" copied (its "copied ✓").
|
|
78
|
-
attachCopiedAt: 0,
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -116,8 +119,9 @@ export function sessionStateFrom(data) {
|
|
|
116
119
|
state.thinking = s.thinking || null;
|
|
117
120
|
// null on a native host (it ignores the level) and from an older worker.
|
|
118
121
|
state.reasoningHistory = s.reasoning_history || null;
|
|
119
|
-
// The
|
|
122
|
+
// The 🧰 chip's (null on full) and the default kit it is read against.
|
|
120
123
|
state.toolkit = s.toolkit || null;
|
|
124
|
+
state.toolkitDefault = s.toolkit_default || null;
|
|
121
125
|
state.parentId = s.parent_id || null;
|
|
122
126
|
state.continues = s.continues || null;
|
|
123
127
|
state.review = s.review || null;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// The owner page's session state in one place: the open session's id
|
|
2
|
+
// (selected), its fields (current, session_state.js) and every card
|
|
3
|
+
// (sessions, the hub's projection). Writes go through update() (and
|
|
4
|
+
// replaceCurrent() for forgetSession's reset); a write marks the redraws
|
|
5
|
+
// that read what changed, and they run synchronously at the end of the
|
|
6
|
+
// outermost batch(), or at once for a bare write.
|
|
7
|
+
// See docs/internals/web-frontend.md ("The session store").
|
|
8
|
+
//
|
|
9
|
+
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
10
|
+
// new export of an old module would leave a cached copy without it.
|
|
11
|
+
|
|
12
|
+
import { FIELDS, statusWrite } from "./session_fields.js";
|
|
13
|
+
|
|
14
|
+
// The redraws, by name, in the one order a flush runs them: the lists
|
|
15
|
+
// before the bar (updateSessionTitle reads the strip's active card), the
|
|
16
|
+
// composer before the bar (as setTurnRunning runs them).
|
|
17
|
+
export const ORDER = [
|
|
18
|
+
"lists",
|
|
19
|
+
"paintActive",
|
|
20
|
+
"settingsPanel", "workingSetStrategy",
|
|
21
|
+
"featureGates",
|
|
22
|
+
"composer",
|
|
23
|
+
"infoBar",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// What a change of the list or of the open session's id redraws: the bar
|
|
27
|
+
// reads the list too (the chain's ← → links, the children chip, the
|
|
28
|
+
// archived button, the card's title, the fresh link's picker).
|
|
29
|
+
const SESSIONS_REDRAWS = ["lists", "infoBar"];
|
|
30
|
+
const SELECTED_REDRAWS = ["paintActive", "composer", "infoBar"];
|
|
31
|
+
|
|
32
|
+
// A flush whose redraws keep writing stops after this many passes.
|
|
33
|
+
export const MAX_PASSES = 3;
|
|
34
|
+
|
|
35
|
+
// @param initial the open session's fields to start from (emptySessionState())
|
|
36
|
+
// @param fields {key: {redraws: [name]}} what each field of `current` redraws
|
|
37
|
+
// (session_fields.js FIELDS by default); a field it doesn't list redraws nothing
|
|
38
|
+
// @param log where a refused batch, a capped flush and a redraw error that
|
|
39
|
+
// can't be thrown go (console by default)
|
|
40
|
+
export function createSessionStore({ initial = {}, fields = FIELDS, log = console } = {}) {
|
|
41
|
+
let selected = null;
|
|
42
|
+
let current = initial;
|
|
43
|
+
let sessions = [];
|
|
44
|
+
const redraws = new Map(ORDER.map((name) => [name, []]));
|
|
45
|
+
const pending = new Set();
|
|
46
|
+
let depth = 0;
|
|
47
|
+
let flushing = false;
|
|
48
|
+
|
|
49
|
+
const enqueue = (names) => names.forEach((name) => pending.add(name));
|
|
50
|
+
const fieldRedraws = (key) => fields[key]?.redraws || [];
|
|
51
|
+
|
|
52
|
+
// Run the pending redraws in ORDER, a pass at a time: a write made during
|
|
53
|
+
// a redraw only enqueues (the flushing flag), and the next pass takes it.
|
|
54
|
+
// A redraw that throws doesn't stop the others; the first error is thrown
|
|
55
|
+
// once the flush is over.
|
|
56
|
+
function flush() {
|
|
57
|
+
if (flushing || depth > 0) return;
|
|
58
|
+
flushing = true;
|
|
59
|
+
let error = null;
|
|
60
|
+
try {
|
|
61
|
+
for (let pass = 0; pending.size > 0; pass += 1) {
|
|
62
|
+
if (pass === MAX_PASSES) {
|
|
63
|
+
log.error(`session store: still redrawing after ${MAX_PASSES} passes: ${[...pending].join(", ")}`);
|
|
64
|
+
pending.clear();
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const names = ORDER.filter((name) => pending.has(name));
|
|
68
|
+
pending.clear();
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
for (const fn of redraws.get(name)) {
|
|
71
|
+
try {
|
|
72
|
+
fn();
|
|
73
|
+
} catch (e) {
|
|
74
|
+
error ??= e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
flushing = false;
|
|
81
|
+
}
|
|
82
|
+
if (error) throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const store = {
|
|
86
|
+
get selected() { return selected; },
|
|
87
|
+
get current() { return current; },
|
|
88
|
+
get sessions() { return sessions; },
|
|
89
|
+
|
|
90
|
+
// Write the given parts: +selected+ (an id or null), +current+ (fields
|
|
91
|
+
// written into the open session's object, which stays the same object),
|
|
92
|
+
// +sessions+ (a new list: one changed in place isn't seen). A changed
|
|
93
|
+
// value (!==) marks its redraws.
|
|
94
|
+
update(patch = {}) {
|
|
95
|
+
if ("selected" in patch && patch.selected !== selected) {
|
|
96
|
+
selected = patch.selected;
|
|
97
|
+
enqueue(SELECTED_REDRAWS);
|
|
98
|
+
}
|
|
99
|
+
if (patch.current) {
|
|
100
|
+
for (const [key, value] of Object.entries(patch.current)) {
|
|
101
|
+
if (current[key] === value) continue;
|
|
102
|
+
current[key] = value;
|
|
103
|
+
enqueue(fieldRedraws(key));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ("sessions" in patch && patch.sessions !== sessions) {
|
|
107
|
+
sessions = patch.sessions;
|
|
108
|
+
enqueue(SESSIONS_REDRAWS);
|
|
109
|
+
}
|
|
110
|
+
flush();
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// The open session's status from +source+ (session_fields.js statusWrite:
|
|
114
|
+
// payload, hub, stream, turnEnd, stop), under that source's rule; a
|
|
115
|
+
// source with a card rule replaces the open session's card too.
|
|
116
|
+
// @param streaming whether the open session's stream is live (the hub's rule)
|
|
117
|
+
// @param running whether a turn runs on the page
|
|
118
|
+
// @return whether the open session's card was replaced (the caller redraws the lists)
|
|
119
|
+
setStatus(value, source, { streaming = false, running = false } = {}) {
|
|
120
|
+
const { apply, card } = statusWrite(source, { streaming, running });
|
|
121
|
+
if (!apply) return false;
|
|
122
|
+
let carded = false;
|
|
123
|
+
store.batch(() => {
|
|
124
|
+
store.update({ current: { status: value } });
|
|
125
|
+
if (!card) return;
|
|
126
|
+
const i = sessions.findIndex((s) => s.id === selected);
|
|
127
|
+
if (i < 0) return;
|
|
128
|
+
const next = sessions.slice();
|
|
129
|
+
next[i] = card(sessions[i], value);
|
|
130
|
+
store.update({ sessions: next });
|
|
131
|
+
carded = true;
|
|
132
|
+
});
|
|
133
|
+
return carded;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// A new open session's fields (forgetSession only): always a new object,
|
|
137
|
+
// so a stale async writer still holding the old one writes into it alone.
|
|
138
|
+
replaceCurrent(state) {
|
|
139
|
+
current = { ...state };
|
|
140
|
+
enqueue(Object.keys(current).flatMap(fieldRedraws));
|
|
141
|
+
flush();
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Run +fn+ (synchronous) with its writes' redraws held to its end; nested
|
|
145
|
+
// batches join the outermost. The redraws run even when +fn+ throws (the
|
|
146
|
+
// page shows what was written), then its error is rethrown. An +fn+ that
|
|
147
|
+
// returns a promise is refused: its writes after an await would land
|
|
148
|
+
// outside the batch, so batch only an async function's synchronous parts.
|
|
149
|
+
batch(fn) {
|
|
150
|
+
depth += 1;
|
|
151
|
+
let result;
|
|
152
|
+
let failed = false;
|
|
153
|
+
let error;
|
|
154
|
+
try {
|
|
155
|
+
result = fn();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
failed = true;
|
|
158
|
+
error = e;
|
|
159
|
+
} finally {
|
|
160
|
+
depth -= 1;
|
|
161
|
+
}
|
|
162
|
+
if (result && typeof result.then === "function") {
|
|
163
|
+
log.error("session store: batch() takes a synchronous function; batch an async one's synchronous parts");
|
|
164
|
+
}
|
|
165
|
+
if (failed) {
|
|
166
|
+
try {
|
|
167
|
+
flush();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
log.error(e);
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
flush();
|
|
174
|
+
return result;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Something the store doesn't hold changed (the clock, versions, the
|
|
178
|
+
// stream): +name+'s redraw runs.
|
|
179
|
+
touch(name) {
|
|
180
|
+
enqueue([name]);
|
|
181
|
+
flush();
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Register +fn+ as (one of) the redraw(s) called +name+ (one of ORDER).
|
|
185
|
+
on(name, fn) {
|
|
186
|
+
if (!redraws.has(name)) throw new Error(`session store: no redraw called ${name}`);
|
|
187
|
+
redraws.get(name).push(fn);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
return store;
|
|
191
|
+
}
|