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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/docs/cli.md +7 -6
  4. data/docs/configuration.md +10 -7
  5. data/docs/internals/prompt-caching.md +1 -1
  6. data/docs/internals/web-frontend.md +10 -9
  7. data/docs/internals/working-set.md +2 -2
  8. data/docs/sessions.md +3 -3
  9. data/docs/testing.md +11 -6
  10. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  11. data/lib/samagotchi/version.rb +1 -1
  12. data/lib/samagotchi/web/app.rb +4 -4
  13. data/lib/samagotchi/web/message_display.rb +16 -0
  14. data/lib/samagotchi/web/public/answer_fold.js +110 -30
  15. data/lib/samagotchi/web/public/app.js +22 -40
  16. data/lib/samagotchi/web/public/bubbles.js +27 -7
  17. data/lib/samagotchi/web/public/chip_popover.js +2 -2
  18. data/lib/samagotchi/web/public/details_motion.js +74 -0
  19. data/lib/samagotchi/web/public/history_reveal.js +2 -2
  20. data/lib/samagotchi/web/public/index.html +75 -54
  21. data/lib/samagotchi/web/public/info_bar.js +11 -12
  22. data/lib/samagotchi/web/public/info_bar_model.js +8 -10
  23. data/lib/samagotchi/web/public/level_chip.js +33 -95
  24. data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
  25. data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
  26. data/lib/samagotchi/web/public/revise_stack.js +9 -3
  27. data/lib/samagotchi/web/public/session_fields.js +4 -4
  28. data/lib/samagotchi/web/public/session_state.js +7 -5
  29. data/lib/samagotchi/web/public/session_store.js +1 -1
  30. data/lib/samagotchi/web/public/settings_chip.js +212 -0
  31. data/lib/samagotchi/web/public/settings_panel.js +121 -0
  32. data/lib/samagotchi/web/public/shell_command.js +26 -0
  33. data/lib/samagotchi/web/public/start_settings.js +25 -88
  34. data/lib/samagotchi/web/public/thinking_chip.js +7 -45
  35. data/lib/samagotchi/web/public/time_gaps.js +5 -2
  36. data/lib/samagotchi/web/public/timing.js +13 -3
  37. data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
  38. data/lib/samagotchi/web/public/turn_block.js +6 -1
  39. data/lib/samagotchi/web/public/turn_events.js +1 -1
  40. data/lib/samagotchi/web/public/turn_html.js +16 -14
  41. data/lib/samagotchi/web/session_create.rb +1 -1
  42. data/lib/samagotchi/web/session_show.rb +6 -3
  43. metadata +5 -1
@@ -1,28 +1,18 @@
1
- // The session bar's LLM context chip: the strategy the next turn runs under
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 instead
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 chip: its text, tooltip and whether the session set its own; null
45
- // without a summary (an older worker).
46
- export function chipModel(summary) {
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 form states what a change
68
- // costs, so a Set is the choice (no confirm step), as level_chip.js's.
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("A budget is a number of tokens from 4000 (4k) to 10000000, or off.");
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
- // The session bar's reasoning history chip: which of its own reasoning the
2
- // session's model gets back from the next turn on ("reasoning all";
3
- // "· session" when the session set its own), and a popover with a level
4
- // select that runs /reasoning-history in the session's worker. The summary
5
- // is the session's reasoning_history (SettingExplained#summary; null on a
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
- anchor: "[data-rh-chip]",
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
- export function chipModel(summary) {
27
- return level.chipModel(CONFIG, summary);
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
- if (stub.parentElement !== historyEl || stub.nextElementSibling !== opening) historyEl.insertBefore(stub, opening);
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: ["thinkingChip", "infoBar"], writers: "stream_handlers.js command_ran" },
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: ["llmContextChip", "workingSetStrategy", "infoBar"],
97
+ redraws: ["settingsPanel", "workingSetStrategy", "infoBar"],
98
98
  writers: "stream_handlers.js command_ran",
99
99
  },
100
- reasoningHistory: { redraws: ["reasoningChip", "infoBar"], writers: "stream_handlers.js command_ran" },
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 llm ctx chip), or null.
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 reasoning history chip), or null (a native host).
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 tools chip's (null on full).
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
- "llmContextChip", "thinkingChip", "reasoningChip", "workingSetStrategy",
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
+ }