samagotchi 0.135.0 → 0.136.1
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 +43 -1
- data/docs/cli.md +7 -6
- data/docs/configuration.md +10 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/web-frontend.md +10 -9
- data/docs/internals/working-set.md +2 -2
- data/docs/sessions.md +3 -3
- data/docs/testing.md +11 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +4 -4
- data/lib/samagotchi/web/message_display.rb +16 -0
- data/lib/samagotchi/web/public/answer_fold.js +110 -30
- data/lib/samagotchi/web/public/app.js +22 -40
- data/lib/samagotchi/web/public/bubbles.js +27 -7
- data/lib/samagotchi/web/public/chip_popover.js +2 -2
- data/lib/samagotchi/web/public/details_motion.js +74 -0
- data/lib/samagotchi/web/public/history_reveal.js +2 -2
- data/lib/samagotchi/web/public/index.html +75 -54
- 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/level_chip.js +33 -95
- data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
- data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
- data/lib/samagotchi/web/public/revise_stack.js +9 -3
- data/lib/samagotchi/web/public/session_fields.js +4 -4
- data/lib/samagotchi/web/public/session_state.js +7 -5
- data/lib/samagotchi/web/public/session_store.js +1 -1
- 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/start_settings.js +25 -88
- 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 +13 -3
- data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
- data/lib/samagotchi/web/public/turn_block.js +6 -1
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +16 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +6 -3
- metadata +5 -1
|
@@ -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
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -41,6 +41,10 @@ export const KEPT = ".plugin-card.warn, .verify-verdict, .bubble.recap";
|
|
|
41
41
|
const ANSWERS = ".bubble.output:not(.report-reply)";
|
|
42
42
|
const UNFINISHED = ".streaming";
|
|
43
43
|
const OPENING = "[data-turn-id]";
|
|
44
|
+
// A fold row (answer_fold.js) just before an opening: a delegate report's
|
|
45
|
+
// that opens its turn, the opening's header (in its range, the stub above it).
|
|
46
|
+
const FOLD_ROW = "answer-fold";
|
|
47
|
+
const headOf = (opening) => (opening.previousElementSibling?.classList.contains(FOLD_ROW) ? opening.previousElementSibling : null);
|
|
44
48
|
const STOPPED = ".bubble.cancel";
|
|
45
49
|
const LABEL_CHARS = 120;
|
|
46
50
|
// The stored key of a stub unfolded by hand (answer_fold.js's list).
|
|
@@ -143,10 +147,11 @@ export function historyTurns(historyEl) {
|
|
|
143
147
|
};
|
|
144
148
|
for (const el of historyEl.children) {
|
|
145
149
|
if (el.classList.contains(STUB_CLASS) || el.classList.contains(ROW_CLASS)) continue;
|
|
150
|
+
if (el.classList.contains(FOLD_ROW) && el.nextElementSibling?.matches(OPENING)) continue;
|
|
146
151
|
const opening = el.matches(OPENING) ? el : el.querySelector(OPENING);
|
|
147
152
|
if (opening || el.matches(NOTE_BOUNDARY)) {
|
|
148
153
|
close();
|
|
149
|
-
turn = { id: opening?.dataset.turnId || null, opening: el, prompt: opening || null, els: [], answer: null, wake: !opening };
|
|
154
|
+
turn = { id: opening?.dataset.turnId || null, opening: el, head: headOf(el), prompt: opening || null, els: [], answer: null, wake: !opening };
|
|
150
155
|
turns.push(turn);
|
|
151
156
|
continue;
|
|
152
157
|
}
|
|
@@ -196,7 +201,8 @@ export function installReviseStacks({
|
|
|
196
201
|
if (stored().includes(stubKey(id))) stub.classList.add("open");
|
|
197
202
|
stubs.set(id, stub);
|
|
198
203
|
}
|
|
199
|
-
|
|
204
|
+
const top = headOf(opening) || opening;
|
|
205
|
+
if (stub.parentElement !== historyEl || stub.nextElementSibling !== top) historyEl.insertBefore(stub, top);
|
|
200
206
|
return stub;
|
|
201
207
|
}
|
|
202
208
|
|
|
@@ -252,7 +258,7 @@ export function installReviseStacks({
|
|
|
252
258
|
const keepStubs = new Set();
|
|
253
259
|
const keepRows = new Set();
|
|
254
260
|
for (const turn of turns) {
|
|
255
|
-
const range = [turn.opening, ...turn.els];
|
|
261
|
+
const range = [turn.head, turn.opening, ...turn.els].filter(Boolean);
|
|
256
262
|
const place = turn.id && superseded.get(turn.id);
|
|
257
263
|
if (place) {
|
|
258
264
|
const stub = stubFor(turn.id, turn.opening, stored);
|
|
@@ -64,7 +64,7 @@ export const FIELDS = {
|
|
|
64
64
|
redraws: ["infoBar"],
|
|
65
65
|
writers: "store.setStatus only (statusWrite below): the hub, the stream, a turn end's re-read, Stop, the payload",
|
|
66
66
|
},
|
|
67
|
-
thinking: { redraws: ["
|
|
67
|
+
thinking: { redraws: ["settingsPanel", "infoBar"], writers: "stream_handlers.js command_ran" },
|
|
68
68
|
model: {
|
|
69
69
|
source: (d) => d.session?.model_name,
|
|
70
70
|
redraws: ["infoBar"],
|
|
@@ -94,11 +94,12 @@ export const FIELDS = {
|
|
|
94
94
|
writers: "followSelectedSession, workerGone",
|
|
95
95
|
},
|
|
96
96
|
llmContext: {
|
|
97
|
-
redraws: ["
|
|
97
|
+
redraws: ["settingsPanel", "workingSetStrategy", "infoBar"],
|
|
98
98
|
writers: "stream_handlers.js command_ran",
|
|
99
99
|
},
|
|
100
|
-
reasoningHistory: { redraws: ["
|
|
100
|
+
reasoningHistory: { redraws: ["settingsPanel", "infoBar"], writers: "stream_handlers.js command_ran" },
|
|
101
101
|
toolkit: { redraws: ["infoBar"], writers: "stream_handlers.js (command_ran, hook_notice)" },
|
|
102
|
+
toolkitDefault: { redraws: ["infoBar"], writers: "the payload" },
|
|
102
103
|
timing: {
|
|
103
104
|
source: (d) => d.timing,
|
|
104
105
|
redraws: ["infoBar"],
|
|
@@ -108,7 +109,6 @@ export const FIELDS = {
|
|
|
108
109
|
turnStarted: { redraws: ["composer", "infoBar"], writers: "setTurnRunning" },
|
|
109
110
|
streamSeq: { redraws: [], writers: "resync" },
|
|
110
111
|
streamFromGuess: { redraws: [], writers: "followWokenWorker, startStream, the stream's takeGuess" },
|
|
111
|
-
attachCopiedAt: { redraws: ["infoBar"], writers: "the info bar's copy button" },
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
// The fields a hub card brings for the open session (followSelectedSession):
|
|
@@ -57,12 +57,15 @@ export function emptySessionState() {
|
|
|
57
57
|
workerFeatures: [],
|
|
58
58
|
bridgeUp: false,
|
|
59
59
|
// The LLM context strategy, apply rule and budget the next turn runs
|
|
60
|
-
// under, each with its source (the info bar's
|
|
60
|
+
// under, each with its source (the info bar's ⚙ chip), or null.
|
|
61
61
|
llmContext: null,
|
|
62
62
|
// Which of its reasoning the model gets back from the next turn on, with
|
|
63
|
-
// its source (the
|
|
63
|
+
// its source (the ⚙ chip's), or null (a native host).
|
|
64
64
|
reasoningHistory: null,
|
|
65
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,
|
|
66
69
|
timing: normalizeTiming(),
|
|
67
70
|
// A turn started on it while the page had it open (setTurnRunning): a
|
|
68
71
|
// fresh link's picker goes at once, before the timing re-read brings the
|
|
@@ -76,8 +79,6 @@ export function emptySessionState() {
|
|
|
76
79
|
// The stream was opened from 0 (a wake with no cursor to go on) and no
|
|
77
80
|
// new turn has started on it yet (app.js followWokenWorker).
|
|
78
81
|
streamFromGuess: false,
|
|
79
|
-
// When the info bar's "copy chi --attach" copied (its "copied ✓").
|
|
80
|
-
attachCopiedAt: 0,
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -118,8 +119,9 @@ export function sessionStateFrom(data) {
|
|
|
118
119
|
state.thinking = s.thinking || null;
|
|
119
120
|
// null on a native host (it ignores the level) and from an older worker.
|
|
120
121
|
state.reasoningHistory = s.reasoning_history || null;
|
|
121
|
-
// The
|
|
122
|
+
// The 🧰 chip's (null on full) and the default kit it is read against.
|
|
122
123
|
state.toolkit = s.toolkit || null;
|
|
124
|
+
state.toolkitDefault = s.toolkit_default || null;
|
|
123
125
|
state.parentId = s.parent_id || null;
|
|
124
126
|
state.continues = s.continues || null;
|
|
125
127
|
state.review = s.review || null;
|
|
@@ -17,7 +17,7 @@ import { FIELDS, statusWrite } from "./session_fields.js";
|
|
|
17
17
|
export const ORDER = [
|
|
18
18
|
"lists",
|
|
19
19
|
"paintActive",
|
|
20
|
-
"
|
|
20
|
+
"settingsPanel", "workingSetStrategy",
|
|
21
21
|
"featureGates",
|
|
22
22
|
"composer",
|
|
23
23
|
"infoBar",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// The session bar's ⚙ chip and its panel: the open session's thinking,
|
|
2
|
+
// reasoning history and LLM context in one place, as the start page's ⚙
|
|
3
|
+
// panel has them for a new session (settings_panel.js draws both). The chip
|
|
4
|
+
// reads "⚙" alone while the session follows the model for all three, else
|
|
5
|
+
// "⚙" and the session's own in the start page's words ("⚙ think low · ctx
|
|
6
|
+
// stale"; class own, the accent). A pick in the panel runs that row's
|
|
7
|
+
// command with --yes in the session's worker at once (one reply line in the
|
|
8
|
+
// conversation, like a typed /thinking low) and the panel stays open; the
|
|
9
|
+
// pressed segment follows the session's summary when the reply's command_ran
|
|
10
|
+
// lands, never the tap. The summaries are the session's thinking,
|
|
11
|
+
// reasoning_history and llm_context (SettingExplained#summary,
|
|
12
|
+
// LLMContextStrategy::Explained#summary: the worker's snapshot, a
|
|
13
|
+
// command_ran, or the server's own reading of the session file).
|
|
14
|
+
//
|
|
15
|
+
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
16
|
+
// new export of an old module would leave a cached copy without it.
|
|
17
|
+
|
|
18
|
+
import { createChipPopover, SHEET_TALL } from "./chip_popover.js";
|
|
19
|
+
import { DEFAULT, commandLine as levelLine, segments } from "./level_chip.js";
|
|
20
|
+
import { BUDGET_ERROR, commandLine as contextLine, ownValues, parseBudget } from "./llm_context_chip.js";
|
|
21
|
+
import { CONFIG as REASONING } from "./reasoning_history_chip.js";
|
|
22
|
+
import { contextSegments, contextWords, settingsPanelTemplate } from "./settings_panel.js";
|
|
23
|
+
import { CONFIG as THINKING } from "./thinking_chip.js";
|
|
24
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
25
|
+
|
|
26
|
+
export const PANEL_LABEL = "Session settings";
|
|
27
|
+
export const NOTE = "From the next turn's start.";
|
|
28
|
+
export const RUNNING_NOTE = "A turn is running: it applies after this turn.";
|
|
29
|
+
const CONTEXT_COST = "Turning forget on or off re-reads the whole prompt once.";
|
|
30
|
+
|
|
31
|
+
// A level's own value ("low"), null when it follows the model.
|
|
32
|
+
const levelOwn = (summary) => summary?.own || null;
|
|
33
|
+
|
|
34
|
+
// The context's own values in a few words ("stale · budget 64000"), null
|
|
35
|
+
// when the session has none.
|
|
36
|
+
const contextOwn = (summary) => (summary?.own ? contextWords(summary.own) : null);
|
|
37
|
+
|
|
38
|
+
function levelTitle(cfg, summary) {
|
|
39
|
+
return `${cfg.name}: ${summary.level} (${summary.own ? "this session" : summary.source || "the model's own"})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function contextTitle(summary) {
|
|
43
|
+
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
44
|
+
return `LLM context: ${summary.strategy} (${summary.strategy_where}); apply ${summary.apply} (${summary.apply_where}); ` +
|
|
45
|
+
`budget ${budget} (${summary.budget_where})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The chip for +summaries+ ({thinking, reasoningHistory, llmContext}, each
|
|
49
|
+
// a summary or null): {text (the own values past ⚙, "" for none), own,
|
|
50
|
+
// title (each value and where it comes from), label (the accessible name:
|
|
51
|
+
// the panel and what is set, settingsLabel's shape)}; null without any
|
|
52
|
+
// summary (an older worker: no chip).
|
|
53
|
+
export function chipModel({ thinking = null, reasoningHistory = null, llmContext = null } = {}) {
|
|
54
|
+
const think = thinking?.level ? thinking : null;
|
|
55
|
+
const rh = reasoningHistory?.level ? reasoningHistory : null;
|
|
56
|
+
const ctx = llmContext?.strategy ? llmContext : null;
|
|
57
|
+
if (!think && !rh && !ctx) return null;
|
|
58
|
+
const set = [
|
|
59
|
+
levelOwn(think) && `${THINKING.prefix} ${levelOwn(think)}`,
|
|
60
|
+
levelOwn(rh) && `${REASONING.prefix} ${levelOwn(rh)}`,
|
|
61
|
+
contextOwn(ctx) && `ctx ${contextOwn(ctx)}`,
|
|
62
|
+
].filter(Boolean);
|
|
63
|
+
const lines = [think && levelTitle(THINKING, think), rh && levelTitle(REASONING, rh), ctx && contextTitle(ctx)].filter(Boolean);
|
|
64
|
+
return {
|
|
65
|
+
text: set.join(" · "),
|
|
66
|
+
own: set.length > 0,
|
|
67
|
+
title: `${lines.join(". ")}. Click to change them for this session.`,
|
|
68
|
+
label: set.length ? `${PANEL_LABEL}: ${set.join(", ")}` : PANEL_LABEL,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
73
|
+
// without a summary. ⚙ is drawn as the start page's settings icon (the
|
|
74
|
+
// glyph stays the text); the own values are a span of their own: a phone
|
|
75
|
+
// shows ⚙ with a dot instead (index.html's CSS).
|
|
76
|
+
export function chipTemplate(summaries) {
|
|
77
|
+
const chip = summaries && chipModel(summaries);
|
|
78
|
+
if (!chip) return nothing;
|
|
79
|
+
return html`<button type="button" class=${`model settings-chip${chip.own ? " own" : ""}`} title=${chip.title}
|
|
80
|
+
aria-label=${chip.label} aria-haspopup="dialog" data-settings-chip><span class="chip-icon sc-gear" aria-hidden="true">⚙</span>${
|
|
81
|
+
chip.text ? html`<span class="sc-own"> ${chip.text}</span>` : nothing}</button>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The panel's note: when a change applies (after the running turn, while
|
|
85
|
+
// one runs) and what each drawn row's change costs.
|
|
86
|
+
export function noteText({ thinking, reasoningHistory, llmContext, running = false }) {
|
|
87
|
+
return [NOTE, running ? RUNNING_NOTE : null, thinking ? THINKING.cost : null, reasoningHistory ? REASONING.cost : null,
|
|
88
|
+
llmContext ? CONTEXT_COST : null].filter(Boolean).join(" ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The panel (settings_panel.js settingsPanelTemplate) over the session's
|
|
92
|
+
// summaries: the Thinking row, the Reasoning history row (a chat host's
|
|
93
|
+
// only), the Context row with its Apply and budget disclosure; each row's
|
|
94
|
+
// pressed segment the session's own. +more+: the disclosure open; +error+:
|
|
95
|
+
// its Set's.
|
|
96
|
+
export function panelTemplate({ thinking = null, reasoningHistory = null, llmContext = null, running = false,
|
|
97
|
+
more = false, error = null }) {
|
|
98
|
+
const think = segments(THINKING, thinking, levelOwn(thinking));
|
|
99
|
+
const rh = segments(REASONING, reasoningHistory, levelOwn(reasoningHistory));
|
|
100
|
+
const ctx = contextSegments(llmContext, llmContext?.own || null);
|
|
101
|
+
return settingsPanelTemplate({
|
|
102
|
+
heading: PANEL_LABEL,
|
|
103
|
+
kind: "this session",
|
|
104
|
+
rows: [
|
|
105
|
+
think ? { name: "think", label: THINKING.name, segments: think, act: "think" } : null,
|
|
106
|
+
// "Reasoning", the chip's word: "Reasoning history" wraps in the label's column.
|
|
107
|
+
rh ? { name: "rh", label: "Reasoning", segments: rh, act: "reasoning" } : null,
|
|
108
|
+
ctx ? { name: "ctx", label: "Context", segments: ctx, act: "ctx",
|
|
109
|
+
more: { open: more, summary: llmContext, choice: llmContext.own || null, error } } : null,
|
|
110
|
+
],
|
|
111
|
+
note: noteText({ thinking: think, reasoningHistory: rh, llmContext: ctx, running }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// What a pick (+act+, a segment's data-act: "think:low", "reasoning:all",
|
|
116
|
+
// "ctx:stale", "ctx:default") runs for +summaries+: the line, or null (the
|
|
117
|
+
// segment pressed already). One field alone (D2): a strategy tap sends the
|
|
118
|
+
// strategy only, the session's own apply and budget stay; the Context
|
|
119
|
+
// row's default is the reset (strategy, apply and budget follow the model
|
|
120
|
+
// again), only when the session has any of its own.
|
|
121
|
+
export function actLine(act, { thinking = null, reasoningHistory = null, llmContext = null } = {}) {
|
|
122
|
+
const [what, value] = act.split(/:(.*)/s);
|
|
123
|
+
if (what === "think") return levelLine(THINKING, value, levelOwn(thinking) || DEFAULT);
|
|
124
|
+
if (what === "reasoning") return levelLine(REASONING, value, levelOwn(reasoningHistory) || DEFAULT);
|
|
125
|
+
if (what !== "ctx") return null;
|
|
126
|
+
if (value === DEFAULT) return llmContext?.own ? contextLine() : null;
|
|
127
|
+
const initial = ownValues(llmContext);
|
|
128
|
+
return contextLine({ ...initial, strategy: value }, initial);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The disclosure's Set: the apply rule and the budget as typed, against the
|
|
132
|
+
// session's own; only the field that changed goes. {line} (null: nothing
|
|
133
|
+
// changed) or {error} (a budget chi wouldn't take).
|
|
134
|
+
export function moreLine({ apply, budget }, llmContext) {
|
|
135
|
+
const typed = String(budget ?? "").trim();
|
|
136
|
+
if (typed && parseBudget(typed) === null) return { error: BUDGET_ERROR };
|
|
137
|
+
const initial = ownValues(llmContext);
|
|
138
|
+
return { line: contextLine({ ...initial, apply, budget: typed }, initial) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The chip's panel on chip_popover.js' shell (a bottom sheet on a phone).
|
|
142
|
+
// el: where the chip is drawn (#infoText, info_bar.js: clicks are
|
|
143
|
+
// delegated); summaries(): the open session's {thinking, reasoningHistory,
|
|
144
|
+
// llmContext} (null: no session); session(): its id; running(): a turn
|
|
145
|
+
// runs; run(line): sends the command.
|
|
146
|
+
// @return {close, refresh, open}
|
|
147
|
+
export function createSettingsControl({ el, summaries, session = () => null, running = () => false, run,
|
|
148
|
+
id = "settingsPopover", anchor = "[data-settings-chip]",
|
|
149
|
+
doc = document, win = window }) {
|
|
150
|
+
// The summaries the open panel is drawn from (each refresh's newer ones),
|
|
151
|
+
// the disclosure and its Set's error.
|
|
152
|
+
let shown = null;
|
|
153
|
+
let more = false;
|
|
154
|
+
let error = null;
|
|
155
|
+
|
|
156
|
+
function draw() {
|
|
157
|
+
render(panelTemplate({ ...shown, running: running(), more, error }), shell.pop);
|
|
158
|
+
shell.replace();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function show(data, opening) {
|
|
162
|
+
shown = data;
|
|
163
|
+
if (opening) {
|
|
164
|
+
const own = data.llmContext?.own;
|
|
165
|
+
more = !!(own?.apply || typeof own?.budget_tokens === "number");
|
|
166
|
+
error = null;
|
|
167
|
+
}
|
|
168
|
+
draw();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function act(name) {
|
|
172
|
+
if (name === "ctx-more") {
|
|
173
|
+
more = !more;
|
|
174
|
+
return draw();
|
|
175
|
+
}
|
|
176
|
+
const line = actLine(name, shown);
|
|
177
|
+
if (line) run(line);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function submit(form) {
|
|
181
|
+
if (!form.classList.contains("sset-ctx-form")) return;
|
|
182
|
+
const result = moreLine({ apply: form.elements.apply.value, budget: form.elements.budget.value }, shown.llmContext);
|
|
183
|
+
error = result.error || null;
|
|
184
|
+
draw();
|
|
185
|
+
if (result.line) run(result.line);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Focus inside on open (the popover sits at the end of <body>): the
|
|
189
|
+
// pressed segment of the first row.
|
|
190
|
+
function focusInside() {
|
|
191
|
+
shell.pop.querySelector(".sset-seg [aria-pressed=true]")?.focus();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const present = () => {
|
|
195
|
+
const data = summaries();
|
|
196
|
+
return data && chipModel(data) ? data : null;
|
|
197
|
+
};
|
|
198
|
+
const shell = createChipPopover({
|
|
199
|
+
id, className: "ctx-popover start-settings session-settings", width: 440, el, anchor, summary: present, session,
|
|
200
|
+
show, sheet: true, sheetShare: SHEET_TALL, modal: true, label: PANEL_LABEL, onOpen: focusInside,
|
|
201
|
+
onClose: () => { shown = null; error = null; }, onSubmit: submit, onAct: act, doc, win,
|
|
202
|
+
});
|
|
203
|
+
// An edit makes the disclosure Set's error stale.
|
|
204
|
+
shell.pop.addEventListener("input", (e) => {
|
|
205
|
+
if (error && e.target.closest(".sset-ctx-form")) {
|
|
206
|
+
error = null;
|
|
207
|
+
draw();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return { close: shell.close, refresh: shell.refresh, get open() { return shell.isOpen(); } };
|
|
212
|
+
}
|