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
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Everything that changes while the page runs is read through deps at the
|
|
6
6
|
// event, never captured at creation: the open session (selected()), its
|
|
7
|
-
// fields (
|
|
7
|
+
// fields (store.current: app.js forgetSession replaces the object), the page's
|
|
8
8
|
// capabilities, the composer's text. A session switch mid-stream is handled
|
|
9
9
|
// as before: the handlers act on whatever is open when the event comes.
|
|
10
10
|
//
|
|
11
11
|
// deps:
|
|
12
12
|
// selected() the open session's id, or null
|
|
13
|
-
//
|
|
13
|
+
// store the page's session store (session_store.js): the
|
|
14
|
+
// open session's fields are read as store.current
|
|
15
|
+
// and written with store.update, one patch per handler
|
|
16
|
+
// (or one batch); a write redraws what reads it (the
|
|
17
|
+
// bar, a chip's popover), so no handler calls a redraw
|
|
14
18
|
// clientId this tab's id (own prompts and commands)
|
|
15
19
|
// capabilities() the page's capabilities (card.js shownElsewhere)
|
|
16
20
|
// historyEl the history element (keepFollowing, the failed-turn redraw)
|
|
17
|
-
// liveTurn, turnDom, shownTexts, questionCards, contextChips
|
|
18
|
-
// llmContextControl, thinkingControl,
|
|
19
|
-
// reasoningHistoryControl the page's views
|
|
21
|
+
// liveTurn, turnDom, shownTexts, questionCards, contextChips the page's views
|
|
20
22
|
// workingSet the stage view's working set (web.working_set),
|
|
21
23
|
// null when off
|
|
22
24
|
// participantsControl the owner's participants panel (participants_panel.js),
|
|
@@ -27,8 +29,9 @@
|
|
|
27
29
|
// close the stream and resync
|
|
28
30
|
// composer: {empty(), refill(text), setChips(chips)}
|
|
29
31
|
// restoreVerify(anchor) a restored Verify turn's claim, armed again for its resend
|
|
32
|
+
// restoreRevise(anchor) a restored Revise turn's draft ({turn_id}), its chip back
|
|
30
33
|
// getSession(id, opts) data.js
|
|
31
|
-
// resync(),
|
|
34
|
+
// resync(), applyPromptOps(data), showSteers(steers),
|
|
32
35
|
// markRecapStale(), setTurnRunning(running), setLiveStatus(status),
|
|
33
36
|
// endTurnView(kind, endText, opts), appendChunk(text), readTurnEnd(),
|
|
34
37
|
// applyFinalMarkdown(messages), refreshTimingFromSession(),
|
|
@@ -56,13 +59,14 @@ import { commandView, drawCommandBubble, historyHasCommandBubble } from "./comma
|
|
|
56
59
|
import { keepEarlyRestore, restoreAction } from "./restore.js";
|
|
57
60
|
import { PAIR, setBadgeMode } from "./participant_names.js";
|
|
58
61
|
import { isParticipantAuthor } from "./participant_model.js";
|
|
62
|
+
import { stillOn } from "./latest.js";
|
|
59
63
|
|
|
60
64
|
export function createStreamHandlers(deps) {
|
|
61
65
|
const {
|
|
62
|
-
selected,
|
|
63
|
-
liveTurn, turnDom, shownTexts, questionCards, contextChips,
|
|
66
|
+
selected, store, clientId, capabilities, historyEl,
|
|
67
|
+
liveTurn, turnDom, shownTexts, questionCards, contextChips,
|
|
64
68
|
workingSet = null, participantsControl = null, sentIds, earlyRestores, stream, composer, getSession,
|
|
65
|
-
resync,
|
|
69
|
+
resync, applyPromptOps, showSteers, markRecapStale, setTurnRunning, setLiveStatus, endTurnView,
|
|
66
70
|
appendChunk, readTurnEnd, applyFinalMarkdown, refreshTimingFromSession, makeUserBubble, addEndBubble,
|
|
67
71
|
commandBubble, addCommandBubble, addNoteBubble, findInTurnRoots, setUsedMemories, showRecap,
|
|
68
72
|
showGuardrailWarning, showHookNotice, onInitEvent, upsertCard, refreshCardBodies, syncModelPick,
|
|
@@ -85,26 +89,21 @@ export function createStreamHandlers(deps) {
|
|
|
85
89
|
const { text, thinking } = routeChunk(data);
|
|
86
90
|
liveTurn.timing.setNote("");
|
|
87
91
|
turnDom.chunk({ text, thinking, iteration: data.iteration ?? null, now: data.replayed ? null : Date.now() });
|
|
88
|
-
const current =
|
|
92
|
+
const current = store.current;
|
|
89
93
|
const pct = extractCtxPct(data, current.ctxWindow, current.llmContext?.budget_tokens);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
updateInfoBar();
|
|
93
|
-
}
|
|
94
|
+
// A chunk with the same % redraws nothing (the store compares).
|
|
95
|
+
if (pct !== null && !Number.isNaN(pct)) store.update({ current: { ctxPct: pct } });
|
|
94
96
|
},
|
|
95
97
|
generation_started: (data) => {
|
|
96
98
|
// The request after a /model's unresolved window started: whatever it
|
|
97
99
|
// brings, the ctx tooltip no longer waits for it (ctxBarTip).
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
current.ctxUnknownAfterSwitch = false;
|
|
100
|
+
// A changed value redraws the bar (the store), an unchanged one not.
|
|
101
|
+
const patch = { ctxUnknownAfterSwitch: false };
|
|
101
102
|
if (data?.context_window_tokens) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
current.ctxWindow = data.context_window_tokens;
|
|
105
|
-
current.ctxWindowSource = source;
|
|
103
|
+
patch.ctxWindow = data.context_window_tokens;
|
|
104
|
+
patch.ctxWindowSource = data.context_window_source || null;
|
|
106
105
|
}
|
|
107
|
-
|
|
106
|
+
store.update({ current: patch });
|
|
108
107
|
liveTurn.timing.setNote("");
|
|
109
108
|
// The step's clock (the composer's delivery line): the page's own at a
|
|
110
109
|
// live start; a replay's comes from the snapshot's generation age,
|
|
@@ -114,15 +113,19 @@ export function createStreamHandlers(deps) {
|
|
|
114
113
|
},
|
|
115
114
|
generation_completed: (data) => {
|
|
116
115
|
liveTurn.timing.setNote("");
|
|
117
|
-
const current =
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
116
|
+
const current = store.current;
|
|
117
|
+
// The generation's speed and the session's token totals, from the
|
|
118
|
+
// server; the rows per model, the server's whole list (the open one
|
|
119
|
+
// with this generation in). An event without them (a replay, an older
|
|
120
|
+
// worker) keeps the page's: the timing object stays, no redraw.
|
|
121
|
+
const patch = {};
|
|
122
|
+
const tokens = generationTokens(current.timing.tokens, data);
|
|
123
|
+
const segments = Array.isArray(data?.model_segments) ? data.model_segments : null;
|
|
124
|
+
if (tokens !== current.timing.tokens || segments) {
|
|
125
|
+
patch.timing = { ...current.timing, tokens, ...(segments ? { modelSegments: segments } : {}) };
|
|
126
|
+
}
|
|
127
|
+
if (data?.served_model) patch.served = [data.served_model, data.requested_model, data.served_expected_by || null];
|
|
128
|
+
store.update({ current: patch });
|
|
126
129
|
turnDom.generationCompleted();
|
|
127
130
|
},
|
|
128
131
|
// Prompts sent while a turn ran merge into it at an iteration boundary:
|
|
@@ -142,14 +145,14 @@ export function createStreamHandlers(deps) {
|
|
|
142
145
|
turn_started: (data) => keepFollowing(historyEl, () => {
|
|
143
146
|
if (stream.takeGuess()) {
|
|
144
147
|
// A replay of a turn the render drew: re-read and stream from there.
|
|
145
|
-
if (replaysDrawnTurn(data,
|
|
148
|
+
if (replaysDrawnTurn(data, store.current.timing.turnRecords)) {
|
|
146
149
|
stream.restart();
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
// A finished turn still in the stage goes first: the start op looks
|
|
151
154
|
// for a queued prompt among its rows.
|
|
152
|
-
turnDom.handOff
|
|
155
|
+
turnDom.handOff({ immediate: true });
|
|
153
156
|
markRecapStale();
|
|
154
157
|
shownTexts.turnStarted();
|
|
155
158
|
const promptBubble = applyPromptOps(data);
|
|
@@ -171,13 +174,19 @@ export function createStreamHandlers(deps) {
|
|
|
171
174
|
if (data.participant_mode) setBadgeMode(promptBubble, data.participant_mode);
|
|
172
175
|
// The owner's input queues behind it (the composer's delivery line).
|
|
173
176
|
liveTurn.participantTurn = participant;
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
// One redraw of the composer, the bar and the lists, before
|
|
178
|
+
// keepFollowing's scroll.
|
|
179
|
+
store.batch(() => {
|
|
180
|
+
setTurnRunning(true);
|
|
181
|
+
setLiveStatus("running");
|
|
182
|
+
});
|
|
176
183
|
liveTurn.timing.detach();
|
|
177
184
|
turnDom.turnStarted({ ...data, promptBubble });
|
|
178
185
|
liveTurn.timing.start(data.started_at, data.turn_id || null);
|
|
179
186
|
}),
|
|
180
187
|
turn_completed: async (data) => {
|
|
188
|
+
// The turn's id: the event names none, its turn_started did.
|
|
189
|
+
const turnId = data.turn_id || liveTurn.timing.turnId || null;
|
|
181
190
|
activityEnded();
|
|
182
191
|
// A participant's turn: their spend and turns in the panel moved.
|
|
183
192
|
if (data.origin?.author) participantsControl?.reload();
|
|
@@ -185,6 +194,13 @@ export function createStreamHandlers(deps) {
|
|
|
185
194
|
const out = turnOutput(data);
|
|
186
195
|
if (out) appendChunk(out);
|
|
187
196
|
if (data.verdict) deps.placeVerdict?.(data.verdict);
|
|
197
|
+
// A Revise turn that revised: its prompt takes the stamp, as a reload
|
|
198
|
+
// draws it, and the stack folds (revise_stack.js follows the DOM).
|
|
199
|
+
if (data.revises?.turn_id && turnId) {
|
|
200
|
+
for (const prompt of findInTurnRoots(".bubble.user[data-turn-id]")) {
|
|
201
|
+
if (prompt.dataset.turnId === turnId) prompt.dataset.revises = data.revises.turn_id;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
188
204
|
// after_turn hooks are running and may present the answer: its
|
|
189
205
|
// answer_display (display: null when they didn't) comes next. A worker
|
|
190
206
|
// that dies before it never sends one: workerGone releases the wait, and
|
|
@@ -193,13 +209,11 @@ export function createStreamHandlers(deps) {
|
|
|
193
209
|
if (display) liveTurn.displayPending = display;
|
|
194
210
|
liveTurn.endRead = readTurnEnd();
|
|
195
211
|
await liveTurn.endRead;
|
|
196
|
-
// The answer
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// never swap in after the pop. The hold reveals by itself after
|
|
200
|
-
// HOLD_MS whatever comes. The stage has no hold.
|
|
212
|
+
// The stage's answer is settled now that the rendered markdown is in
|
|
213
|
+
// it (or the re-read failed), and the display if one was coming, so
|
|
214
|
+
// the hand-off never moves it before its links swap in.
|
|
201
215
|
if (display) await display.promise;
|
|
202
|
-
turnDom.answerReady
|
|
216
|
+
turnDom.answerReady();
|
|
203
217
|
// A participant's review turn: Apply under its reply, as a reload shows it.
|
|
204
218
|
const review = liveTurn.review;
|
|
205
219
|
liveTurn.review = null;
|
|
@@ -212,11 +226,12 @@ export function createStreamHandlers(deps) {
|
|
|
212
226
|
// other session's answers alone.
|
|
213
227
|
answer_display: async (data) => {
|
|
214
228
|
const id = selected();
|
|
229
|
+
const still = stillOn(selected, id);
|
|
215
230
|
try {
|
|
216
231
|
await liveTurn.endRead;
|
|
217
|
-
if (!id ||
|
|
232
|
+
if (!id || !still() || (!data.display && !data.refs)) return;
|
|
218
233
|
const d = await getSession(id, { tail: true, recent: true });
|
|
219
|
-
if (
|
|
234
|
+
if (!still()) return;
|
|
220
235
|
if (Array.isArray(d.messages)) applyFinalMarkdown(d.messages);
|
|
221
236
|
} catch (_) {
|
|
222
237
|
} finally {
|
|
@@ -247,9 +262,11 @@ export function createStreamHandlers(deps) {
|
|
|
247
262
|
prompt_restored: async (data) => {
|
|
248
263
|
const id = selected();
|
|
249
264
|
keepEarlyRestore(data, earlyRestores, { myId: clientId, sentIds });
|
|
250
|
-
const { refill, label, images: restoredImages, verify } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
265
|
+
const { refill, label, images: restoredImages, verify, revises } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
251
266
|
if (refill) composer.refill(refill);
|
|
252
267
|
if (refill && verify) deps.restoreVerify?.(verify);
|
|
268
|
+
// A Revise turn's draft too: its chip comes back with the text.
|
|
269
|
+
if (refill && revises) deps.restoreRevise?.(revises);
|
|
253
270
|
if (restoredImages?.length) composer.setChips(restoredChips(selected(), restoredImages));
|
|
254
271
|
const failedText = liveTurn.failedText;
|
|
255
272
|
liveTurn.failedText = null;
|
|
@@ -276,40 +293,24 @@ export function createStreamHandlers(deps) {
|
|
|
276
293
|
// A queued /working-set line is no longer pending (the list's pill).
|
|
277
294
|
workingSet?.commandRan(data);
|
|
278
295
|
const view = commandView(data, clientId);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
// A fresh link's picker shows the model the worker kept, a refused
|
|
285
|
-
// /model too (its pick set the button before the answer).
|
|
286
|
-
if (view.modelName) syncModelPick?.();
|
|
296
|
+
// What the command changed, in one patch: the bar and the chips'
|
|
297
|
+
// popovers redraw once (the store; the working set's list follows
|
|
298
|
+
// /llm-context: workingSetStrategy).
|
|
299
|
+
const patch = {};
|
|
300
|
+
if (view.modelName && view.modelName !== store.current.model) Object.assign(patch, { model: view.modelName, served: null });
|
|
287
301
|
// /llm-context changed what the next turn runs under.
|
|
288
|
-
if (data.llm_context)
|
|
289
|
-
state().llmContext = data.llm_context;
|
|
290
|
-
updateInfoBar();
|
|
291
|
-
llmContextControl.refresh();
|
|
292
|
-
// The working set's list offers pin only under forget.
|
|
293
|
-
workingSet?.strategyChanged();
|
|
294
|
-
}
|
|
302
|
+
if (data.llm_context) patch.llmContext = data.llm_context;
|
|
295
303
|
// /thinking (or /model) changed the level the next turn runs at.
|
|
296
|
-
if (data.thinking)
|
|
297
|
-
state().thinking = data.thinking;
|
|
298
|
-
updateInfoBar();
|
|
299
|
-
thinkingControl.refresh();
|
|
300
|
-
}
|
|
304
|
+
if (data.thinking) patch.thinking = data.thinking;
|
|
301
305
|
// /reasoning-history (or /model) changed it; null: the model now runs
|
|
302
306
|
// on a native host, which ignores it (the chip goes).
|
|
303
|
-
if ("reasoning_history" in data)
|
|
304
|
-
state().reasoningHistory = data.reasoning_history || null;
|
|
305
|
-
updateInfoBar();
|
|
306
|
-
reasoningHistoryControl.refresh();
|
|
307
|
-
}
|
|
307
|
+
if ("reasoning_history" in data) patch.reasoningHistory = data.reasoning_history || null;
|
|
308
308
|
// /toolkit changed what the next turn declares; null: full (no chip).
|
|
309
|
-
if ("toolkit" in data)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
if ("toolkit" in data) patch.toolkit = data.toolkit || null;
|
|
310
|
+
store.update({ current: patch });
|
|
311
|
+
// A fresh link's picker shows the model the worker kept, a refused
|
|
312
|
+
// /model too (its pick set the button before the answer): an event (D9).
|
|
313
|
+
if (view.modelName) syncModelPick?.();
|
|
313
314
|
if (view.resync) {
|
|
314
315
|
await resync();
|
|
315
316
|
// Another session was opened during the re-read: the bubble isn't its.
|
|
@@ -375,34 +376,33 @@ export function createStreamHandlers(deps) {
|
|
|
375
376
|
// ignored with web.working_set off.
|
|
376
377
|
working_set_updated: (data) => { workingSet?.update({ items: data.items, at: data.at, ...(data.adds ? { adds: data.adds } : {}) }); },
|
|
377
378
|
context_status: (data) => {
|
|
378
|
-
const current =
|
|
379
|
+
const current = store.current;
|
|
380
|
+
const patch = {};
|
|
379
381
|
// After /model (Engine#announce_switched_context!): the new model's
|
|
380
382
|
// window, and usage null when it didn't resolve (the % clears, "ctx ?").
|
|
381
383
|
// Only that event carries the key: a null drops the old model's window,
|
|
382
384
|
// so the chunks before the next window count no % against it.
|
|
383
385
|
if (data && "context_window_tokens" in data) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
+
patch.ctxWindow = data.context_window_tokens || null;
|
|
387
|
+
patch.ctxWindowSource = data.context_window_tokens ? data.context_window_source || null : null;
|
|
386
388
|
// The ctx tooltip's "until the next request" (ctxBarTip).
|
|
387
|
-
|
|
389
|
+
patch.ctxUnknownAfterSwitch = !data.context_window_tokens;
|
|
388
390
|
}
|
|
389
391
|
if (data && "usage" in data && data.usage === null) {
|
|
390
|
-
current
|
|
391
|
-
updateInfoBar();
|
|
392
|
+
store.update({ current: { ...patch, ctxPct: null } });
|
|
392
393
|
return;
|
|
393
394
|
}
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
395
|
+
const ctxWindow = "ctxWindow" in patch ? patch.ctxWindow : current.ctxWindow;
|
|
396
|
+
const pct = extractCtxPct(data, ctxWindow, current.llmContext?.budget_tokens);
|
|
397
|
+
if (pct !== null) patch.ctxPct = pct;
|
|
398
|
+
store.update({ current: patch });
|
|
399
399
|
},
|
|
400
400
|
question_requested: (data) => {
|
|
401
401
|
const pq = data.pending_question;
|
|
402
402
|
if (!pq || !pq.id) return;
|
|
403
403
|
// A plugin's refs on the reply that asks: on its step before the card
|
|
404
404
|
// draws the asking slot.
|
|
405
|
-
if (pq.refs) turnDom.questionRefs
|
|
405
|
+
if (pq.refs) turnDom.questionRefs(pq.refs);
|
|
406
406
|
questionCards.renderQuestion(pq);
|
|
407
407
|
},
|
|
408
408
|
question_answered: (data) => {
|
|
@@ -417,13 +417,12 @@ export function createStreamHandlers(deps) {
|
|
|
417
417
|
// the server's whole side block, for the ctx tooltip's line at once.
|
|
418
418
|
side_usage: (data) => {
|
|
419
419
|
if (!data?.side || typeof data.side !== "object") return;
|
|
420
|
-
|
|
421
|
-
updateInfoBar();
|
|
420
|
+
store.update({ current: { timing: { ...store.current.timing, side: data.side } } });
|
|
422
421
|
},
|
|
423
422
|
// One collected just as a turn started describes the chat before it.
|
|
424
423
|
recap_ready: (data) => {
|
|
425
424
|
// After the turn it covers, never above it.
|
|
426
|
-
turnDom.handOff
|
|
425
|
+
turnDom.handOff({ immediate: true });
|
|
427
426
|
if (!liveTurn.running) showRecap(data.recap, 0, data.created_at || null);
|
|
428
427
|
},
|
|
429
428
|
guardrail_warning: (data) => showGuardrailWarning(data.message, data.label || "guardrails"),
|
|
@@ -433,8 +432,7 @@ export function createStreamHandlers(deps) {
|
|
|
433
432
|
hook_notice: (data) => {
|
|
434
433
|
// A plugin put the session on another toolkit (ctx.use_toolkit).
|
|
435
434
|
if ("toolkit" in data) {
|
|
436
|
-
|
|
437
|
-
updateInfoBar();
|
|
435
|
+
store.update({ current: { toolkit: data.toolkit || null } });
|
|
438
436
|
}
|
|
439
437
|
if (shownElsewhere(data, capabilities())) return;
|
|
440
438
|
if (!turnDom.notice(data)) showHookNotice(data);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// and the stops in flight. The in-flight set lives here, not in the DOM:
|
|
5
5
|
// the stage rebuilds its "now" slot, and a redraw must keep a clicked
|
|
6
6
|
// button disabled. Pure but for the injected calls, so `node --test`
|
|
7
|
-
// covers it;
|
|
7
|
+
// covers it; turn_block.js and stage_view.js draw the button.
|
|
8
8
|
import { stopTask as postStopTask } from "./data.js";
|
|
9
9
|
import { clip } from "./format.js";
|
|
10
10
|
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// it came from are the session's thinking (SettingExplained#summary:
|
|
6
|
-
// {level, source, own}; the worker's snapshot, a command_ran, or the
|
|
7
|
-
// server's own reading of the session file). A config on level_chip.js.
|
|
8
|
-
//
|
|
9
|
-
// The start page's new session sets its level in the ⚙ panel instead
|
|
10
|
-
// (start_settings.js, over LEVELS).
|
|
1
|
+
// Thinking as a level setting (level_chip.js): the levels the settings
|
|
2
|
+
// panels' Thinking row offers (settings_panel.js; the start page's ⚙ panel,
|
|
3
|
+
// the session bar's, settings_chip.js) and the /thinking line a session's
|
|
4
|
+
// pick runs in its worker.
|
|
11
5
|
|
|
12
6
|
import * as level from "./level_chip.js";
|
|
13
|
-
import { html, nothing } from "./vendor/lit-html.js";
|
|
14
7
|
|
|
15
8
|
export const LEVELS = ["off", "low", "medium", "high"];
|
|
16
9
|
|
|
@@ -19,42 +12,11 @@ export const CONFIG = Object.freeze({
|
|
|
19
12
|
prefix: "think",
|
|
20
13
|
command: "/thinking",
|
|
21
14
|
levels: LEVELS,
|
|
22
|
-
|
|
23
|
-
formClass: "think-form",
|
|
24
|
-
popoverClass: "think-popover",
|
|
25
|
-
nowClass: "think-now",
|
|
26
|
-
when: () => "From the next turn's start. On Gemma the whole prompt is read again; a hosted API reads the whole conversation again.",
|
|
15
|
+
cost: "A thinking change: on Gemma the whole prompt is read again; a hosted API reads the whole conversation again.",
|
|
27
16
|
});
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// The chip: its text, tooltip and whether the session set its own; null
|
|
32
|
-
// without a summary (an older worker). The default with nothing set
|
|
33
|
-
// anywhere shows too ("think default"): the chip is where the level is set.
|
|
34
|
-
export function chipModel(summary) {
|
|
35
|
-
return level.chipModel(CONFIG, summary);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
39
|
-
// without a summary.
|
|
40
|
-
export function chipTemplate(summary) {
|
|
41
|
-
const chip = chipModel(summary);
|
|
42
|
-
if (!chip) return nothing;
|
|
43
|
-
return html`<button type="button" class=${`model think-chip${chip.own ? " own" : ""}`} title=${chip.title} data-think-chip aria-haspopup="dialog">${chip.text}</button>`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// The /thinking line for a select value, with --yes (level_chip.js
|
|
47
|
-
// commandLine); null when it is what the form opened with.
|
|
18
|
+
// The /thinking line for a level, with --yes (level_chip.js commandLine);
|
|
19
|
+
// null when it is the session's (+initial+).
|
|
48
20
|
export function commandLine(choice, initial = null) {
|
|
49
21
|
return level.commandLine(CONFIG, choice, initial);
|
|
50
22
|
}
|
|
51
|
-
|
|
52
|
-
// The popover's form (level_chip.js formTemplate).
|
|
53
|
-
export function formTemplate(summary, opts) {
|
|
54
|
-
return level.formTemplate(CONFIG, summary, opts);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// The chip's popover (level_chip.js createLevelControl).
|
|
58
|
-
export function createThinkingControl({ id = "thinkingPopover", ...opts }) {
|
|
59
|
-
return level.createLevelControl({ cfg: CONFIG, id, ...opts });
|
|
60
|
-
}
|
|
@@ -28,7 +28,7 @@ export function timeGapEl(text) {
|
|
|
28
28
|
|
|
29
29
|
// The history items a group (timing.js turnGroups) holds, by index.
|
|
30
30
|
function groupIndexes(group) {
|
|
31
|
-
if (group.kind === "note" || group.kind === "steer") return [group.i];
|
|
31
|
+
if (group.kind === "note" || group.kind === "steer" || group.kind === "command") return [group.i];
|
|
32
32
|
if (group.kind === "answer") return [group.answer];
|
|
33
33
|
const steps = group.steps || [];
|
|
34
34
|
return [
|
|
@@ -54,11 +54,14 @@ export function groupTimes(group, items) {
|
|
|
54
54
|
|
|
55
55
|
// For each group, the divider text before it, or null. A group with no
|
|
56
56
|
// known time draws none and hides the gap across it (no divider measured
|
|
57
|
-
// over a stretch whose times are unknown).
|
|
57
|
+
// over a stretch whose times are unknown). A `!cmd`'s record is saved with
|
|
58
|
+
// no time: it draws none and the gap is measured across it (it ran in that
|
|
59
|
+
// stretch).
|
|
58
60
|
export function groupGaps(groups, items, now = Date.now()) {
|
|
59
61
|
let prevEnd = null;
|
|
60
62
|
return groups.map((group) => {
|
|
61
63
|
const { start, end } = groupTimes(group, items);
|
|
64
|
+
if (group.kind === "command" && start === null) return null;
|
|
62
65
|
const text = prevEnd !== null && start !== null ? gapText(prevEnd, start, now) : null;
|
|
63
66
|
prevEnd = end;
|
|
64
67
|
return text;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isShellCommandItem } from "./shell_command.js";
|
|
2
|
+
|
|
1
3
|
export function formatDuration(milliseconds) {
|
|
2
4
|
const ms = Number(milliseconds);
|
|
3
5
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
@@ -148,8 +150,10 @@ export function turnRecordAt(timing, index) {
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
// A history item that starts a turn: a prompt, or the note a context wake
|
|
151
|
-
// turn starts with (turn_start).
|
|
152
|
-
|
|
153
|
+
// turn starts with (turn_start). A `!cmd`'s record (shell_command.js) is a
|
|
154
|
+
// user message but no prompt: no turn, no record, no timing.
|
|
155
|
+
const startsTurn = (item) =>
|
|
156
|
+
(item?.role === "user" && !isShellCommandItem(item)) || (item?.role === "note" && item.turn_start === true);
|
|
153
157
|
|
|
154
158
|
// For each history item, the turn index its timing shows under, or null. A
|
|
155
159
|
// turn with tool calls holds several assistant messages; only the last one
|
|
@@ -173,7 +177,7 @@ export function timedTurnIndexes(items) {
|
|
|
173
177
|
return result;
|
|
174
178
|
}
|
|
175
179
|
|
|
176
|
-
// The history as turns, for the turn
|
|
180
|
+
// The history as turns, for the turn block's reload: notes on their own, and
|
|
177
181
|
// per prompt a turn with its record, its steps (one per iteration: the
|
|
178
182
|
// turn's tool records grouped by iteration, and the assistant messages but
|
|
179
183
|
// the last, in order; a text-less iteration leaves no message, so a text
|
|
@@ -181,7 +185,9 @@ export function timedTurnIndexes(items) {
|
|
|
181
185
|
// assistant message). Assistant messages before any prompt are answers on
|
|
182
186
|
// their own. A turn that ended with no answer (the server's empty_answer
|
|
183
187
|
// item) has no answer: its texts are steps, and `emptyAnswer` is that
|
|
184
|
-
// item's index.
|
|
188
|
+
// item's index. A `!cmd`'s record is a group of its own (`command`, its
|
|
189
|
+
// index): no steps, no record, no turn index. The turn index counts
|
|
190
|
+
// prompts, as timedTurnIndexes does. The
|
|
185
191
|
// user's lines merged into the running turn (`merged`, the server's
|
|
186
192
|
// Steer::INPUT_KIND) stay in it: the turn's `merged` list.
|
|
187
193
|
export function turnGroups(items, timing) {
|
|
@@ -191,6 +197,10 @@ export function turnGroups(items, timing) {
|
|
|
191
197
|
items.forEach((item, i) => {
|
|
192
198
|
if (item?.role === "note" && !startsTurn(item)) {
|
|
193
199
|
groups.push({ kind: "note", i });
|
|
200
|
+
} else if (isShellCommandItem(item)) {
|
|
201
|
+
// A `!cmd`'s record: its bubble alone, between the turns.
|
|
202
|
+
groups.push({ kind: "command", i });
|
|
203
|
+
turn = null;
|
|
194
204
|
} else if (item?.role === "steer") {
|
|
195
205
|
if (turn) turn.steers.push(i);
|
|
196
206
|
else groups.push({ kind: "steer", i });
|
|
@@ -267,7 +277,7 @@ function turnGroup(g, items, timing) {
|
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
// A canceled turn that ran more than one step or any call has no answer:
|
|
270
|
-
// its partial text stays in the block, as it does live (
|
|
280
|
+
// its partial text stays in the block, as it does live (turn_block.js
|
|
271
281
|
// turnEnded promotes only a completed turn or a lone step with no tools). A
|
|
272
282
|
// failed turn never answered: its texts are steps.
|
|
273
283
|
function canceledSteps(g, record, tools) {
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
// The session bar's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// session
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
1
|
+
// The session bar's 🧰 chip, after ⚙: the session's toolkit. "🧰" alone
|
|
2
|
+
// while the session is on config's default kit (toolkit.default, full
|
|
3
|
+
// unless set; GET /api/sessions/:id's session.toolkit_default), "🧰 lean"
|
|
4
|
+
// on another; the tooltip names the kit, the tools it leaves out and how to
|
|
5
|
+
// change them. No popover: /toolkit changes it. The summary is the
|
|
6
|
+
// session's toolkit ({name, label, left_out, reachable, removed, fallback}:
|
|
7
|
+
// Toolkit::Explained#summary, null on full; the worker's snapshot or a
|
|
8
|
+
// command_ran; the server's own reading of the session file has no lists).
|
|
9
|
+
// Left out is reachable (find_tools and call_tool reach them) and removed
|
|
10
|
+
// (only /toolkit add brings one back). A kit with a prompt text has it on
|
|
11
|
+
// the summary too (prompt), and the tooltip ends with it.
|
|
10
12
|
|
|
11
13
|
import { html, nothing } from "./vendor/lit-html.js";
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const FULL = "full";
|
|
16
|
+
const CHANGE = "/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.";
|
|
17
|
+
|
|
18
|
+
// The chip's text ("" while on the default kit: 🧰 alone), tooltip and
|
|
19
|
+
// whether it is the default. +summary+: the session's (null: full);
|
|
20
|
+
// +defaultKit+: config's toolkit.default (null: not known, full).
|
|
21
|
+
export function chipModel(summary, defaultKit = null) {
|
|
22
|
+
const fallbackKit = defaultKit || FULL;
|
|
23
|
+
if (!summary) {
|
|
24
|
+
const isDefault = fallbackKit === FULL;
|
|
25
|
+
const title = isDefault ? `Toolkit: ${FULL} (the default), every tool is declared. ${CHANGE}`
|
|
26
|
+
: `Toolkit: ${FULL}, every tool is declared (the default is ${fallbackKit}). ${CHANGE}`;
|
|
27
|
+
return { text: isDefault ? "" : FULL, title, isDefault };
|
|
28
|
+
}
|
|
16
29
|
const label = summary.label || summary.name;
|
|
17
|
-
const
|
|
30
|
+
const isDefault = !summary.fallback && label === fallbackKit;
|
|
31
|
+
const parts = [`Toolkit: ${label}${isDefault ? " (the default)" : ""}, the tools this session declares.`];
|
|
18
32
|
if (summary.fallback) parts.push(`${summary.fallback}: every tool is declared (full) until it resolves.`);
|
|
19
33
|
if (Array.isArray(summary.groups)) {
|
|
20
34
|
parts.push(`Source groups: ${summary.groups.length ? summary.groups.join(", ") : "none"}.`);
|
|
@@ -27,15 +41,17 @@ export function chipModel(summary) {
|
|
|
27
41
|
} else if (Array.isArray(summary.left_out)) {
|
|
28
42
|
parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
|
|
29
43
|
}
|
|
30
|
-
parts.push(
|
|
44
|
+
parts.push(CHANGE);
|
|
31
45
|
if (summary.prompt) parts.push(`Prompt: ${summary.prompt}`);
|
|
32
|
-
return { text:
|
|
46
|
+
return { text: isDefault ? "" : `${label}${summary.fallback ? " ?" : ""}`, title: parts.join(" "), isDefault };
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
// The session bar's chip (a lit-html template; info_bar.js)
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!
|
|
40
|
-
|
|
49
|
+
// The session bar's chip (a lit-html template; info_bar.js): +toolkit+ is
|
|
50
|
+
// {summary, defaultKit}; nothing without it. 🧰 is drawn as the start
|
|
51
|
+
// page's toolkit icon (the glyph stays the text; index.html's CSS).
|
|
52
|
+
export function chipTemplate(toolkit) {
|
|
53
|
+
if (!toolkit) return nothing;
|
|
54
|
+
const chip = chipModel(toolkit.summary, toolkit.defaultKit);
|
|
55
|
+
return html`<span class=${`model tools-chip${chip.isDefault ? " default" : ""}`} title=${chip.title}><span
|
|
56
|
+
class="chip-icon tc-kit" aria-hidden="true">🧰</span>${chip.text ? ` ${chip.text}` : ""}</span>`;
|
|
41
57
|
}
|