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
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// refresh() fetch the list (no events stream)
|
|
20
20
|
// select(id) open a session
|
|
21
21
|
// confirmDelete(id), setArchived(id, archive), archiveOne(id, archive)
|
|
22
|
-
//
|
|
22
|
+
// updateSessionTitle(), showToast(text, action)
|
|
23
23
|
// allViewOpen(), openAllView(), leaveAllView(hash)
|
|
24
24
|
// renameShown(owner) whether a card's title has its ✎ (session_title.js)
|
|
25
25
|
// onRename() the active card's ✎ clicked (session_rename.js open)
|
|
@@ -231,7 +231,7 @@ function draw(el, template) {
|
|
|
231
231
|
export function createListsView(deps) {
|
|
232
232
|
const {
|
|
233
233
|
doc, win, topStripEl, allListEl, sessions, selected, scope, scopeError, eventsLive, refresh, select,
|
|
234
|
-
confirmDelete, setArchived, archiveOne,
|
|
234
|
+
confirmDelete, setArchived, archiveOne, updateSessionTitle, showToast,
|
|
235
235
|
allViewOpen, openAllView, leaveAllView, renameShown = () => false, onRename = () => {}, renaming = () => false,
|
|
236
236
|
} = deps;
|
|
237
237
|
const $ = (s) => doc.querySelector(s);
|
|
@@ -302,18 +302,25 @@ export function createListsView(deps) {
|
|
|
302
302
|
// is the cards drawn, which select mode picks from.
|
|
303
303
|
const drawnOrder = new Map(); // list element → card ids in the order last drawn
|
|
304
304
|
const heldLists = new Set(); // lists drawn in a held order
|
|
305
|
+
// Lists whose last draw was hidden (the strip under the all view, which
|
|
306
|
+
// the store's list write draws as the view opens): that order was never
|
|
307
|
+
// seen, so a hover that stuck on the strip doesn't hold it afterwards.
|
|
308
|
+
const drawnHidden = new Set();
|
|
305
309
|
const canHover = win.matchMedia?.("(hover: hover)").matches ?? false;
|
|
306
310
|
function listOrder(el, next, idOf = (s) => s.id) {
|
|
307
311
|
// The strip's popover holds it too (the pointer leaves the strip for it,
|
|
308
312
|
// and a touch screen has no hover at all), and so does a rename typed
|
|
309
313
|
// over its active card (the card doesn't jump away under the input).
|
|
310
|
-
const
|
|
314
|
+
const shown = el.getClientRects().length > 0;
|
|
315
|
+
const holding = shown
|
|
311
316
|
&& ((canHover && el.matches(":hover")) || (el === topStripEl && (familyPop.isOpen() || renaming())) || chainPopIn(el));
|
|
312
|
-
const held = holding ? drawnOrder.get(el) : null;
|
|
317
|
+
const held = holding && !drawnHidden.has(el) ? drawnOrder.get(el) : null;
|
|
313
318
|
const ordered = held ? heldOrder(held, next, idOf) : next;
|
|
314
319
|
if (held && ordered.some((x, i) => idOf(x) !== idOf(next[i]))) heldLists.add(el);
|
|
315
320
|
else heldLists.delete(el);
|
|
316
321
|
drawnOrder.set(el, ordered.map(idOf));
|
|
322
|
+
if (shown) drawnHidden.delete(el);
|
|
323
|
+
else drawnHidden.add(el);
|
|
317
324
|
return ordered;
|
|
318
325
|
}
|
|
319
326
|
|
|
@@ -498,7 +505,7 @@ export function createListsView(deps) {
|
|
|
498
505
|
noteChainFamilies(cards);
|
|
499
506
|
const refused = scopeError();
|
|
500
507
|
if (refused) {
|
|
501
|
-
draw(topStripEl, html`<div class="hint scope-error">${refused} · <a href=${allScopeHref(""
|
|
508
|
+
draw(topStripEl, html`<div class="hint scope-error">${refused} · <a href=${allScopeHref("")}>show every session</a></div>`);
|
|
502
509
|
lastDrawn.delete(topStripEl);
|
|
503
510
|
familyPop.close();
|
|
504
511
|
chainPop.close();
|
|
@@ -727,14 +734,9 @@ export function createListsView(deps) {
|
|
|
727
734
|
selecting.running = { done: 0, total: ids.length, archive };
|
|
728
735
|
updateSelectBar();
|
|
729
736
|
try {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
render();
|
|
734
|
-
updateInfoBar();
|
|
735
|
-
}
|
|
736
|
-
return answer;
|
|
737
|
-
}, (done) => {
|
|
737
|
+
// An archived one flips in the store's list (archiveOne): the lists
|
|
738
|
+
// and the bar redraw from there.
|
|
739
|
+
const outcome = await runBatch(ids, (id) => archiveOne(id, archive), (done) => {
|
|
738
740
|
selecting.running.done = done;
|
|
739
741
|
updateSelectBar();
|
|
740
742
|
});
|
|
@@ -105,6 +105,8 @@ export function createLiveTiming({
|
|
|
105
105
|
|
|
106
106
|
start(startedAt = new Date(now()).toISOString(), id = null) {
|
|
107
107
|
stopTicker();
|
|
108
|
+
// In place, on purpose: the running turn's own clock in the session
|
|
109
|
+
// store's timing (session_fields.js, D8).
|
|
108
110
|
timing().activeTurn = id ? { id, started_at: startedAt } : { started_at: startedAt };
|
|
109
111
|
turnId = id || null;
|
|
110
112
|
// The turns ended so far, so this one comes next.
|
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
// The session
|
|
2
|
-
// ("llm ctx stale,forget"; "· session" when the session set its own), and a
|
|
3
|
-
// popover that sets the session's own strategy, apply rule and budget by
|
|
4
|
-
// running /llm-context in its worker (the command's reply shows in the
|
|
5
|
-
// conversation, like a typed one). The values and where each came from are
|
|
6
|
-
// the session's llm_context (LLMContextStrategy::Explained#summary: the
|
|
1
|
+
// The session's LLM context (LLMContextStrategy::Explained#summary: the
|
|
7
2
|
// worker's snapshot, a command_ran, or the server's own reading of the
|
|
8
|
-
// session file)
|
|
3
|
+
// session file) as the settings panels read and change it: the session's own
|
|
4
|
+
// values (ownValues), the /llm-context line a session bar's pick runs in its
|
|
5
|
+
// worker (commandLine; settings_chip.js), a typed budget (parseBudget).
|
|
9
6
|
//
|
|
10
|
-
// The start page's new session sets its strategy in the ⚙ panel
|
|
7
|
+
// The start page's new session sets its strategy in the ⚙ panel
|
|
11
8
|
// (start_settings.js): mergeChoice and choiceWords are its, a choice in the
|
|
12
9
|
// session file's shape sent as POST /api/sessions' llm_context words.
|
|
13
10
|
|
|
14
|
-
import { createChipPopover } from "./chip_popover.js";
|
|
15
|
-
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
16
|
-
|
|
17
|
-
const STRATEGIES = [
|
|
18
|
-
{ value: "none", label: "none" },
|
|
19
|
-
{ value: "stale", label: "stale" },
|
|
20
|
-
{ value: "stale,forget", label: "stale + forget (experimental)" },
|
|
21
|
-
];
|
|
22
11
|
export const APPLIES = ["payoff", "next_request", "turn_end"];
|
|
23
|
-
export const SESSION = "session";
|
|
24
12
|
|
|
25
13
|
const DEFAULT = "default";
|
|
14
|
+
// What a budget parseBudget refuses gets, under the fields.
|
|
15
|
+
export const BUDGET_ERROR = "A budget is a number of tokens from 4000 (4k) to 10000000, or off.";
|
|
26
16
|
const LAYER_ORDER = ["stale", "forget"];
|
|
27
17
|
|
|
28
18
|
// Layers as one value, in chi's order (stale before forget); unknown ones after.
|
|
@@ -41,31 +31,12 @@ export function ownValues(summary) {
|
|
|
41
31
|
return { strategy, apply: own.apply || DEFAULT, budget };
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
// The
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
if (!summary || !summary.strategy) return null;
|
|
48
|
-
const own = summary.strategy_source === "session" || !!summary.own;
|
|
49
|
-
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
50
|
-
const title = `LLM context strategy: ${summary.strategy} (${summary.strategy_where}); apply ${summary.apply} ` +
|
|
51
|
-
`(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for this session.`;
|
|
52
|
-
return { text: `llm ctx ${summary.strategy}${own ? ` · ${SESSION}` : ""}`, title, own };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
56
|
-
// without a summary.
|
|
57
|
-
export function chipTemplate(summary) {
|
|
58
|
-
const chip = chipModel(summary);
|
|
59
|
-
if (!chip) return nothing;
|
|
60
|
-
return html`<button type="button" class=${`model llmctx-chip${chip.own ? " own" : ""}`} title=${chip.title} aria-haspopup="dialog">${chip.text}</button>`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// The /llm-context line for the form's values. With +initial+ (the values
|
|
64
|
-
// the form opened with) only the fields the user changed go, so a value the
|
|
65
|
-
// form can't show is never unset by a Set; null when nothing changed.
|
|
34
|
+
// The /llm-context line for the panel's values. With +initial+ (the
|
|
35
|
+
// session's own values, ownValues) only the fields that differ go, so a
|
|
36
|
+
// value the tap didn't touch is never unset; null when nothing changed.
|
|
66
37
|
// Without it every field goes, "default" for one left to the model, and all
|
|
67
|
-
// of them default is a reset. With --yes: the
|
|
68
|
-
// costs, so a
|
|
38
|
+
// of them default is a reset. With --yes: the panel states what a change
|
|
39
|
+
// costs, so a pick is the choice (no confirm step), as level_chip.js's.
|
|
69
40
|
export function commandLine({ strategy = DEFAULT, apply = DEFAULT, budget = "" } = {}, initial = null) {
|
|
70
41
|
const words = { strategy, apply, budget: String(budget).trim() || DEFAULT };
|
|
71
42
|
const was = initial && { strategy: initial.strategy, apply: initial.apply, budget: String(initial.budget).trim() || DEFAULT };
|
|
@@ -104,7 +75,7 @@ export function mergeChoice(choice, { strategy = DEFAULT, apply = DEFAULT, budge
|
|
|
104
75
|
else if (field === "apply") next.apply = word;
|
|
105
76
|
else {
|
|
106
77
|
const tokens = parseBudget(word);
|
|
107
|
-
if (tokens === null) throw new Error(
|
|
78
|
+
if (tokens === null) throw new Error(BUDGET_ERROR);
|
|
108
79
|
next.budget_tokens = tokens;
|
|
109
80
|
}
|
|
110
81
|
}
|
|
@@ -122,105 +93,3 @@ export function choiceWords(choice) {
|
|
|
122
93
|
if (typeof choice.budget_tokens === "number") words.budget = choice.budget_tokens > 0 ? String(choice.budget_tokens) : "off";
|
|
123
94
|
return Object.keys(words).length ? words : undefined;
|
|
124
95
|
}
|
|
125
|
-
|
|
126
|
-
// The strategy list, with the session's own value when it isn't one of them
|
|
127
|
-
// (forget alone, a newer chi's layer).
|
|
128
|
-
function strategyOptions(chosen) {
|
|
129
|
-
if (chosen === DEFAULT || STRATEGIES.some((s) => s.value === chosen)) return STRATEGIES;
|
|
130
|
-
return [...STRATEGIES, { value: chosen, label: chosen }];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const option = (value, label) => html`<option value=${value}>${label}</option>`;
|
|
134
|
-
|
|
135
|
-
// The popover's form: the values now (each with its source), and the
|
|
136
|
-
// session's own to change.
|
|
137
|
-
export function nowText(summary) {
|
|
138
|
-
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
139
|
-
return `Now: ${summary.strategy} (${summary.strategy_where}) · apply ${summary.apply} (${summary.apply_where}) · ` +
|
|
140
|
-
`budget ${budget} (${summary.budget_where})`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// The popover's form (a lit-html template). +now+ is the summary its "Now:"
|
|
144
|
-
// line reads (refresh passes a newer one, the rest stays as the form opened,
|
|
145
|
-
// so lit leaves what the user typed alone). The selects take their value as a property (.value), set after their
|
|
146
|
-
// options: +unset+ draws the options alone, the first pass renderForm makes.
|
|
147
|
-
export function formTemplate(summary, { running = false, now = summary, unset = false } = {}) {
|
|
148
|
-
const own = ownValues(summary);
|
|
149
|
-
const follow = (field) => (summary[`${field}_source`] === "session" ? "follow the model" : `follow the model (${summary[field]})`);
|
|
150
|
-
const when = "From the next turn's start. Turning forget on or off re-reads the whole prompt once.";
|
|
151
|
-
const chosen = (value) => (unset ? nothing : value);
|
|
152
|
-
return html`<form class="llmctx-form"><div class="ctx-pop-head"><strong>LLM context</strong
|
|
153
|
-
><span class="ctx-pop-kind">this session</span
|
|
154
|
-
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
155
|
-
><div class="ctx-pop-meta llmctx-now">${nowText(now)}</div
|
|
156
|
-
><label class="llmctx-field"><span>Strategy</span><span class="select-wrap"><select name="strategy" .value=${chosen(own.strategy)}>${
|
|
157
|
-
option(DEFAULT, follow("strategy"))}${strategyOptions(own.strategy).map((s) => option(s.value, s.label))}</select></span></label
|
|
158
|
-
><label class="llmctx-field"><span>Apply</span><span class="select-wrap"><select name="apply" .value=${chosen(own.apply)}>${
|
|
159
|
-
option(DEFAULT, follow("apply"))}${APPLIES.map((a) => option(a, a))}</select></span></label
|
|
160
|
-
><label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric" autocomplete="off"
|
|
161
|
-
placeholder="follow the model (64000, 64k or off)" .value=${own.budget}></label
|
|
162
|
-
><div class="ctx-pop-meta">${when}${running ? " A turn is running: it applies after this turn." : ""}</div
|
|
163
|
-
><div class="ctx-pop-actions"><button type="button" class="ctx-pop-detach" data-act="reset">follow the model</button
|
|
164
|
-
><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Draws the form into +target+ (formTemplate's options). A first draw (no
|
|
168
|
-
// form there yet) makes two passes, the options and then the selects'
|
|
169
|
-
// values: lit sets an element's own bindings before its children, so a
|
|
170
|
-
// select's .value would land before its options exist. A later draw is
|
|
171
|
-
// one pass, and lit writes only what changed since the last (the "Now:"
|
|
172
|
-
// line), never a value the user changed.
|
|
173
|
-
export function renderForm(target, summary, opts = {}) {
|
|
174
|
-
if (!target.firstElementChild) render(formTemplate(summary, { ...opts, unset: true }), target);
|
|
175
|
-
render(formTemplate(summary, opts), target);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// The chip's popover (chip_popover.js' shell). el: where the chip is drawn
|
|
179
|
-
// (#infoText, info_bar.js: clicks are delegated); summary(): the open
|
|
180
|
-
// session's llm_context; session(): the open session's id; running(): a
|
|
181
|
-
// turn runs; run(line): sends the command. id and anchor: the popover's id
|
|
182
|
-
// and the chip's selector.
|
|
183
|
-
export function createLlmContextControl({ el, summary, session = () => null, running = () => false, run,
|
|
184
|
-
id = "llmContextPopover", anchor = ".llmctx-chip",
|
|
185
|
-
doc = document, win = window }) {
|
|
186
|
-
// The values the form opened with.
|
|
187
|
-
let initial = null;
|
|
188
|
-
// What the open form was drawn from: the summary, the form's options and
|
|
189
|
-
// the summary its "Now:" line reads.
|
|
190
|
-
let shown = null;
|
|
191
|
-
const draw = (changes = {}) => {
|
|
192
|
-
shown = { ...shown, ...changes };
|
|
193
|
-
renderForm(shell.pop, shown.data, { ...shown.opts, now: shown.now });
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// A refresh (a newer summary): an open form keeps what the user typed;
|
|
197
|
-
// only its "Now:" line follows (the same template again: lit writes that
|
|
198
|
-
// line alone).
|
|
199
|
-
function show(data, opening) {
|
|
200
|
-
if (!opening) return draw({ now: data });
|
|
201
|
-
initial = ownValues(data);
|
|
202
|
-
shown = { data, opts: { running: running() } };
|
|
203
|
-
draw({ now: data });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function submit(form) {
|
|
207
|
-
const values = { strategy: form.elements.strategy.value, apply: form.elements.apply.value,
|
|
208
|
-
budget: form.elements.budget.value };
|
|
209
|
-
const line = commandLine(values, initial);
|
|
210
|
-
shell.close();
|
|
211
|
-
if (line) run(line);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function act(name) {
|
|
215
|
-
if (name !== "reset") return;
|
|
216
|
-
shell.close();
|
|
217
|
-
run(commandLine());
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const shell = createChipPopover({
|
|
221
|
-
id, className: "ctx-popover llmctx-popover", width: 360, el, anchor, summary, session, show,
|
|
222
|
-
onClose: () => { initial = null; shown = null; }, onSubmit: submit, onAct: act, doc, win,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return { close: shell.close, refresh: shell.refresh, get open() { return shell.isOpen(); } };
|
|
226
|
-
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// steps' texts.
|
|
9
9
|
const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
|
|
10
10
|
// The closed steps' texts still shown raw: a live turn's (a reload's come
|
|
11
|
-
// rendered,
|
|
11
|
+
// rendered, turn_block.js textTemplate).
|
|
12
12
|
const RAW_STEPS = ".gen:not(.live) > .gen-text:not(.markdown)";
|
|
13
13
|
|
|
14
14
|
export function createMarkdownPlace({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// chi web's JSON as notifications.js and push_devices.js call it:
|
|
2
|
+
// (method, path, body). Over the page's api (data.js), so a signed-out
|
|
3
|
+
// proxy answer (a 401, a redirect, HTML) reports auth_lost as every other
|
|
4
|
+
// read does, and a refusal throws api()'s Error (the server's words, .status,
|
|
5
|
+
// .code).
|
|
6
|
+
|
|
7
|
+
import { api as pageApi } from "./data.js";
|
|
8
|
+
|
|
9
|
+
// @param body the JSON body (undefined: none)
|
|
10
|
+
// @param opts api()'s fetch options (specs: fetchImpl); api: another client
|
|
11
|
+
// @return a Promise of the answer's JSON ({} for an empty one)
|
|
12
|
+
export function methodApi(method, path, body, { api = pageApi, ...opts } = {}) {
|
|
13
|
+
return api(path, { ...opts, method, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
14
|
+
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
isWaitingTag, openPushKeys, pushSupported, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe,
|
|
37
37
|
} from "./push.js";
|
|
38
38
|
import { createPushDevices } from "./push_devices.js";
|
|
39
|
+
import { methodApi } from "./method_api.js";
|
|
39
40
|
|
|
40
41
|
export const NOTIFY_KEY = "chi_notify";
|
|
41
42
|
// The device id chi web gave this app's subscription (POST subscribe's
|
|
@@ -64,17 +65,9 @@ const PUSH_TITLES = {
|
|
|
64
65
|
denied: "Notifications are blocked for this app in the phone's settings",
|
|
65
66
|
};
|
|
66
67
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
method,
|
|
71
|
-
headers: body === undefined ? {} : { "Content-Type": "application/json" },
|
|
72
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
73
|
-
});
|
|
74
|
-
const data = res.status === 204 ? null : await res.json().catch(() => null);
|
|
75
|
-
if (!res.ok) throw new Error(data?.detail || `HTTP ${res.status}`);
|
|
76
|
-
return data;
|
|
77
|
-
}
|
|
68
|
+
// chi web's JSON, or api()'s Error; a signed-out answer reports auth_lost
|
|
69
|
+
// (method_api.js).
|
|
70
|
+
const defaultApi = (method, path, body) => methodApi(method, path, body);
|
|
78
71
|
|
|
79
72
|
// How a presence report goes: a beacon when the page is leaving (it
|
|
80
73
|
// outlives the page; same origin, so the cookie goes), else the api's
|
|
@@ -171,6 +171,16 @@ function scheduleReload() {
|
|
|
171
171
|
reloadTimer = setTimeout(load, 120);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// The types the participant stream sends: exactly Web::ParticipantView::EVENTS
|
|
175
|
+
// (spec/web/participant_view_spec.rb reads this list), so a type the server
|
|
176
|
+
// starts sending isn't dropped here unseen.
|
|
177
|
+
const STREAM_EVENTS = [
|
|
178
|
+
"generation_chunk", "generation_retrying", "tool_call_started", "tool_call_completed",
|
|
179
|
+
"turn_started", "turn_completed", "turn_canceled", "turn_failed", "turn_enqueued", "turn_dequeued", "input_merged",
|
|
180
|
+
"question_requested", "question_answered", "question_cancelled", "continue_offered", "continue_resolved",
|
|
181
|
+
"snapshot", "reset", "verdict_recorded",
|
|
182
|
+
];
|
|
183
|
+
|
|
174
184
|
// The stream from the review data's cursor; a closed or refused stream is
|
|
175
185
|
// opened again from fresh data (a worker may have started or restarted).
|
|
176
186
|
function connect() {
|
|
@@ -203,12 +213,7 @@ function connect() {
|
|
|
203
213
|
renderLive();
|
|
204
214
|
}
|
|
205
215
|
};
|
|
206
|
-
for (const type of
|
|
207
|
-
"turn_started", "turn_completed", "turn_canceled", "turn_failed", "turn_enqueued", "turn_dequeued", "input_merged",
|
|
208
|
-
"question_requested", "question_answered", "question_cancelled", "continue_offered", "continue_resolved",
|
|
209
|
-
"snapshot", "reset", "verdict_recorded"]) {
|
|
210
|
-
source.addEventListener(type, onEvent);
|
|
211
|
-
}
|
|
216
|
+
for (const type of STREAM_EVENTS) source.addEventListener(type, onEvent);
|
|
212
217
|
source.onerror = () => {
|
|
213
218
|
if (source.readyState !== EventSource.CLOSED) return;
|
|
214
219
|
scheduleRetry(retryDelay(failures));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The open session's participants by author ("p:<id>" → name), for the
|
|
2
2
|
// author badges on their prompts (turn_events.js promptOps, a reload's
|
|
3
|
-
//
|
|
3
|
+
// turn_block.js): the participants panel sets it when it reads the list. A
|
|
4
4
|
// prompt's own author_name (a saved message, turn_started) comes first; a
|
|
5
5
|
// badge drawn before the list came, with neither, says UNNAMED until
|
|
6
6
|
// nameUnnamedBadges names it.
|
|
@@ -18,6 +18,7 @@ import { html, nothing, render } from "./vendor/lit-html.js";
|
|
|
18
18
|
import { createChipPopover } from "./chip_popover.js";
|
|
19
19
|
import { authorColor, participantAuthor } from "./participant_model.js";
|
|
20
20
|
import { setParticipantNames } from "./participant_names.js";
|
|
21
|
+
import { stillOn } from "./latest.js";
|
|
21
22
|
|
|
22
23
|
// Seen this recently: here now (the chip's dot). The gate writes
|
|
23
24
|
// last_used_at at most once a minute, so two minutes.
|
|
@@ -298,9 +299,10 @@ export function createParticipantsControl({ el, session, api, onChange = () => {
|
|
|
298
299
|
onChange();
|
|
299
300
|
return;
|
|
300
301
|
}
|
|
302
|
+
const still = stillOn(session, id);
|
|
301
303
|
try {
|
|
302
304
|
const body = await api.list(id);
|
|
303
|
-
if (
|
|
305
|
+
if (!still()) return;
|
|
304
306
|
grants = Array.isArray(body.participants) ? body.participants : [];
|
|
305
307
|
working = typeof body.warning === "string" ? body.warning : null;
|
|
306
308
|
inviteMode = typeof body.mode === "string" ? body.mode : null;
|
|
@@ -309,7 +311,7 @@ export function createParticipantsControl({ el, session, api, onChange = () => {
|
|
|
309
311
|
loadedFor = id;
|
|
310
312
|
if (setParticipantNames(grants)) onNames();
|
|
311
313
|
} catch {
|
|
312
|
-
if (
|
|
314
|
+
if (!still()) return;
|
|
313
315
|
// Another session's list and warning are not this one's.
|
|
314
316
|
if (loadedFor !== id) {
|
|
315
317
|
grants = [];
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
// The project picker opened by the top-bar scope chip. The server supplies
|
|
2
|
-
// the project roots and their current/default marks; links carry
|
|
3
|
-
//
|
|
2
|
+
// the project roots and their current/default marks; links never carry a
|
|
3
|
+
// session hash into another project's list.
|
|
4
4
|
|
|
5
5
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
6
6
|
import { allScopeHref, projectScopeHref } from "./scope.js";
|
|
7
7
|
import { createChipPopover } from "./chip_popover.js";
|
|
8
|
-
|
|
9
|
-
function homeHref(search) {
|
|
10
|
-
const view = new URLSearchParams(search || "").get("view");
|
|
11
|
-
return view ? `/?view=${encodeURIComponent(view)}` : "/";
|
|
12
|
-
}
|
|
8
|
+
import { createLatest } from "./latest.js";
|
|
13
9
|
|
|
14
10
|
function sessionCount(count) {
|
|
15
11
|
const n = Number(count) || 0;
|
|
16
12
|
return `${n} session${n === 1 ? "" : "s"}`;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
function projectRow(row
|
|
15
|
+
function projectRow(row) {
|
|
20
16
|
return html`<a class=${`project-row${row.default ? " default" : ""}${row.current ? " current" : ""}`}
|
|
21
|
-
href=${row.default ?
|
|
17
|
+
href=${row.default ? "/" : projectScopeHref(row.dir, "")}
|
|
22
18
|
aria-current=${row.current ? "page" : nothing}>
|
|
23
19
|
<span class="project-main"><span class="project-name">${row.name}</span
|
|
24
20
|
>${row.default ? html`<span class="project-default">default</span>` : nothing
|
|
@@ -30,14 +26,14 @@ function projectRow(row, search) {
|
|
|
30
26
|
|
|
31
27
|
// +rows+ is the API's ordered list. When there is no project scope, the
|
|
32
28
|
// all-projects row is current; a bad ?dir= has no current project to mark.
|
|
33
|
-
export function pickerTemplate({ rows = [], loading = false, error = "",
|
|
29
|
+
export function pickerTemplate({ rows = [], loading = false, error = "", currentDir = null,
|
|
34
30
|
allCurrent = false } = {}) {
|
|
35
|
-
const allHref = allScopeHref("", currentDir
|
|
31
|
+
const allHref = allScopeHref("", currentDir);
|
|
36
32
|
return html`<div class="ctx-pop-head"><strong>Projects</strong><button type="button" class="ctx-pop-close"
|
|
37
33
|
data-act="close" aria-label="Close">×</button></div>
|
|
38
34
|
${loading ? html`<div class="project-note" role="status">Loading projects…</div>` : nothing}
|
|
39
35
|
${error ? html`<div class="project-note project-error" role="alert">${error}</div>` : nothing}
|
|
40
|
-
<nav class="project-list" aria-label="Projects">${rows.map((row) => projectRow(row
|
|
36
|
+
<nav class="project-list" aria-label="Projects">${rows.map((row) => projectRow(row))}
|
|
41
37
|
<a id="projectAll" class=${`project-row project-all${allCurrent ? " current" : ""}`}
|
|
42
38
|
href=${allHref} aria-current=${allCurrent ? "page" : nothing}>
|
|
43
39
|
<span class="project-main"><span class="project-name">All projects</span
|
|
@@ -47,20 +43,20 @@ export function pickerTemplate({ rows = [], loading = false, error = "", search
|
|
|
47
43
|
</nav>`;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
// +scopeError+ returns the page's current folder error, if any.
|
|
51
|
-
|
|
52
|
-
export function createProjectPicker({ el, scope, scopeError = () => null, api, where = () => ({ search: "" }),
|
|
46
|
+
// +scopeError+ returns the page's current folder error, if any.
|
|
47
|
+
export function createProjectPicker({ el, scope, scopeError = () => null, api,
|
|
53
48
|
doc = globalThis.document, win = globalThis.window }) {
|
|
54
49
|
let rows = [];
|
|
55
50
|
let loading = false;
|
|
56
51
|
let error = "";
|
|
57
|
-
|
|
52
|
+
// Only the newest read is drawn; a close drops the one out (latest.js).
|
|
53
|
+
const latest = createLatest();
|
|
58
54
|
|
|
59
55
|
const currentDir = () => scopeError() ? null : scope.dir || null;
|
|
60
56
|
const paint = (focus = false) => {
|
|
61
57
|
const failedScope = !!scopeError();
|
|
62
58
|
render(pickerTemplate({
|
|
63
|
-
rows, loading, error,
|
|
59
|
+
rows, loading, error, currentDir: currentDir(),
|
|
64
60
|
allCurrent: !scope.dir && !failedScope,
|
|
65
61
|
}), shell.pop);
|
|
66
62
|
if (focus) {
|
|
@@ -71,7 +67,7 @@ export function createProjectPicker({ el, scope, scopeError = () => null, api, w
|
|
|
71
67
|
};
|
|
72
68
|
|
|
73
69
|
async function load(focus = false) {
|
|
74
|
-
const
|
|
70
|
+
const fresh = latest.begin();
|
|
75
71
|
loading = true;
|
|
76
72
|
error = "";
|
|
77
73
|
if (shell.isOpen()) {
|
|
@@ -83,14 +79,14 @@ export function createProjectPicker({ el, scope, scopeError = () => null, api, w
|
|
|
83
79
|
const dir = currentDir();
|
|
84
80
|
const answer = await api(`/api/projects${dir ? `?dir=${encodeURIComponent(dir)}` : ""}`);
|
|
85
81
|
if (!Array.isArray(answer)) throw new Error("the server returned an invalid project list");
|
|
86
|
-
if (
|
|
82
|
+
if (!fresh()) return;
|
|
87
83
|
rows = answer;
|
|
88
84
|
} catch (e) {
|
|
89
|
-
if (
|
|
85
|
+
if (!fresh()) return;
|
|
90
86
|
error = `Can't read projects: ${e?.message || e}`;
|
|
91
87
|
rows = [];
|
|
92
88
|
}
|
|
93
|
-
if (
|
|
89
|
+
if (!fresh()) return;
|
|
94
90
|
loading = false;
|
|
95
91
|
if (shell.isOpen()) {
|
|
96
92
|
paint(focus);
|
|
@@ -108,7 +104,7 @@ export function createProjectPicker({ el, scope, scopeError = () => null, api, w
|
|
|
108
104
|
paint();
|
|
109
105
|
void load(true);
|
|
110
106
|
},
|
|
111
|
-
onClose: () => {
|
|
107
|
+
onClose: () => { latest.drop(); },
|
|
112
108
|
});
|
|
113
109
|
|
|
114
110
|
return { pop: shell.pop, isOpen: shell.isOpen, load };
|
|
@@ -91,7 +91,7 @@ export function createQuestionCards({
|
|
|
91
91
|
summary.textContent = summaryText(pq);
|
|
92
92
|
card.appendChild(summary);
|
|
93
93
|
// A hand toggle: the resolve's auto-collapse then leaves it as the user
|
|
94
|
-
// left it (rememberToggle-style, as
|
|
94
|
+
// left it (rememberToggle-style, as turn_block.js does).
|
|
95
95
|
summary.addEventListener("click", () => { card.dataset.toggled = "1"; });
|
|
96
96
|
|
|
97
97
|
// Relayed to the parent: say where else it waits (QuestionDesk#annotate).
|
|
@@ -632,7 +632,7 @@ export function createQuestionCards({
|
|
|
632
632
|
pendingQuestion: () => pending,
|
|
633
633
|
renderContinue: renderContinueCard,
|
|
634
634
|
resolveContinue: resolveContinueCard,
|
|
635
|
-
// The turn
|
|
635
|
+
// The running turn's DOM was reset: a later offer draws a new card.
|
|
636
636
|
forgetContinue() { continueEl = null; },
|
|
637
637
|
};
|
|
638
638
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
// session's model gets back from the next turn on
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// native host, which ignores it, or from an older worker: no chip then).
|
|
7
|
-
// A config on level_chip.js.
|
|
1
|
+
// Reasoning history as a level setting (level_chip.js): which of its own
|
|
2
|
+
// reasoning the session's model gets back from the next turn on. The session
|
|
3
|
+
// bar's ⚙ panel draws its row on a chat host (settings_chip.js; the summary
|
|
4
|
+
// is null on a native host, which ignores it, or from an older worker: no
|
|
5
|
+
// row then) and runs /reasoning-history in the session's worker.
|
|
8
6
|
|
|
9
7
|
import * as level from "./level_chip.js";
|
|
10
|
-
import { html, nothing } from "./vendor/lit-html.js";
|
|
11
8
|
|
|
12
9
|
export const LEVELS = ["drop", "turn", "all"];
|
|
13
10
|
|
|
@@ -16,34 +13,11 @@ export const CONFIG = Object.freeze({
|
|
|
16
13
|
prefix: "reasoning",
|
|
17
14
|
command: "/reasoning-history",
|
|
18
15
|
levels: LEVELS,
|
|
19
|
-
|
|
20
|
-
formClass: "rh-form",
|
|
21
|
-
popoverClass: "rh-popover",
|
|
22
|
-
nowClass: "rh-now",
|
|
23
|
-
when: () => "From the next turn's start. A change to or from all makes the server read the conversation again.",
|
|
16
|
+
cost: "A reasoning history change to or from all makes the server read the conversation again.",
|
|
24
17
|
});
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
31
|
-
// without a summary.
|
|
32
|
-
export function chipTemplate(summary) {
|
|
33
|
-
const chip = chipModel(summary);
|
|
34
|
-
if (!chip) return nothing;
|
|
35
|
-
return html`<button type="button" class=${`model rh-chip${chip.own ? " own" : ""}`} title=${chip.title} data-rh-chip aria-haspopup="dialog">${chip.text}</button>`;
|
|
36
|
-
}
|
|
37
|
-
|
|
19
|
+
// The /reasoning-history line for a level, with --yes; null when it is the
|
|
20
|
+
// session's (+initial+).
|
|
38
21
|
export function commandLine(choice, initial = null) {
|
|
39
22
|
return level.commandLine(CONFIG, choice, initial);
|
|
40
23
|
}
|
|
41
|
-
|
|
42
|
-
export function formTemplate(summary, opts) {
|
|
43
|
-
return level.formTemplate(CONFIG, summary, opts);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// The chip's popover in the session bar (level_chip.js createLevelControl).
|
|
47
|
-
export function createReasoningHistoryControl({ id = "reasoningHistoryPopover", ...opts }) {
|
|
48
|
-
return level.createLevelControl({ cfg: CONFIG, id, ...opts });
|
|
49
|
-
}
|
|
@@ -21,6 +21,8 @@ export function restoreAction(event, { myId, sentIds, composerEmpty }) {
|
|
|
21
21
|
own,
|
|
22
22
|
label: own ? null : clientLabel(origin.client_id),
|
|
23
23
|
...(back && event.verify ? { verify: event.verify } : {}),
|
|
24
|
+
// A Revise turn's draft ({turn_id}): its composer chip comes back.
|
|
25
|
+
...(back && event.revises?.turn_id ? { revises: { turn_id: event.revises.turn_id } } : {}),
|
|
24
26
|
}, back ? event.images : null);
|
|
25
27
|
}
|
|
26
28
|
|