samagotchi 0.131.0 → 0.133.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 +88 -1
- data/bin/chi +19 -1
- data/docs/architecture.md +59 -1
- data/docs/cli.md +29 -8
- data/docs/configuration.md +115 -16
- data/docs/desktop.md +16 -6
- data/docs/guardrails.md +10 -4
- data/docs/internals/anthropic.md +3 -2
- data/docs/internals/engine.md +4 -4
- data/docs/internals/prompt-caching.md +26 -6
- data/docs/memory.md +16 -2
- data/docs/plugins.md +68 -1
- data/docs/sessions.md +60 -6
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +35 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
- data/lib/samagotchi/bundles/model-notes/manifest.yml +4 -1
- data/lib/samagotchi/bundles/model-notes/model_notes_tone.md +3 -0
- data/lib/samagotchi/bundles/model-notes/notes/README.md +14 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +18 -0
- data/lib/samagotchi/chi_home.rb +311 -0
- data/lib/samagotchi/chi_shim.rb +77 -0
- data/lib/samagotchi/child_env.rb +5 -3
- data/lib/samagotchi/config.rb +2 -0
- data/lib/samagotchi/context_fetch.rb +4 -2
- data/lib/samagotchi/desktop/macos.rb +100 -18
- data/lib/samagotchi/engine.rb +35 -4
- data/lib/samagotchi/engine_snapshots.rb +5 -0
- data/lib/samagotchi/gem_update.rb +19 -10
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +21 -2
- data/lib/samagotchi/guardrails/pair_command.rb +57 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +6 -3
- data/lib/samagotchi/guardrails/review_paths.rb +7 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/home_command.rb +145 -0
- data/lib/samagotchi/home_doctor.rb +177 -0
- data/lib/samagotchi/home_notice.rb +80 -0
- data/lib/samagotchi/installed_versions.rb +5 -3
- data/lib/samagotchi/llm_context_command.rb +53 -7
- data/lib/samagotchi/llm_context_override.rb +3 -2
- data/lib/samagotchi/plugin/api.rb +45 -1
- data/lib/samagotchi/plugin/context.rb +34 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/prompt_sections.rb +124 -0
- data/lib/samagotchi/project_env.rb +360 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +33 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +68 -19
- data/lib/samagotchi/session_manager.rb +54 -1
- data/lib/samagotchi/session_model_settings.rb +5 -2
- data/lib/samagotchi/system_prompt.rb +34 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +22 -3
- data/lib/samagotchi/testing/host.rb +22 -1
- data/lib/samagotchi/testing/recorder.rb +6 -1
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tools/builtins.rb +13 -2
- data/lib/samagotchi/update_command.rb +65 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +61 -1
- data/lib/samagotchi/web/public/app.js +4 -1
- data/lib/samagotchi/web/public/chip_popover.js +5 -2
- data/lib/samagotchi/web/public/composer.css +11 -1
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/index.html +52 -10
- data/lib/samagotchi/web/public/level_chip.js +22 -38
- data/lib/samagotchi/web/public/llm_context_chip.js +30 -73
- data/lib/samagotchi/web/public/model_pick.js +26 -0
- data/lib/samagotchi/web/public/model_picker.js +3 -1
- data/lib/samagotchi/web/public/review_start.js +13 -57
- data/lib/samagotchi/web/public/send_turn.js +3 -2
- data/lib/samagotchi/web/public/start_page.js +131 -99
- data/lib/samagotchi/web/public/start_settings.js +298 -0
- data/lib/samagotchi/web/public/start_toolkit.js +157 -0
- data/lib/samagotchi/web/public/thinking_chip.js +9 -22
- data/lib/samagotchi/web/server.rb +27 -8
- data/lib/samagotchi/web/update_check.rb +3 -2
- metadata +11 -1
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chipModel as llmContextChipModel, choiceWords, createLlmContextControl, NEW_SESSION, withChoice,
|
|
3
|
-
} from "./llm_context_chip.js";
|
|
4
|
-
import { chipModel as thinkingChipModel, createThinkingControl, withChoice as withThinkingChoice } from "./thinking_chip.js";
|
|
1
|
+
import { choiceWords } from "./llm_context_chip.js";
|
|
5
2
|
import { MODEL_KEY, modelsFailedText } from "./model_pick.js";
|
|
6
3
|
import { createModelPicker } from "./model_picker.js";
|
|
7
4
|
import { chainNeighbours, freshLink } from "./chains.js";
|
|
8
5
|
import { readStored } from "./storage.js";
|
|
9
|
-
import {
|
|
6
|
+
import { createStartSettings, overrides, settingsLabel } from "./start_settings.js";
|
|
7
|
+
import { createStartToolkit, toolkitLabel, toolkitOverride } from "./start_toolkit.js";
|
|
10
8
|
|
|
11
9
|
// ── The start page's model picker ──────────────────────────────────────────
|
|
12
10
|
// The hosts' models from GET /api/models, the choice this browser made last
|
|
@@ -19,22 +17,29 @@ import { createReviewStartControl, reviewChipModel } from "./review_start.js";
|
|
|
19
17
|
// default in charge. The search list itself is model_picker.js'; a pick
|
|
20
18
|
// sends the focus to the message box.
|
|
21
19
|
//
|
|
22
|
-
// Beside it, the start page's
|
|
23
|
-
//
|
|
24
|
-
// create, then dropped; never remembered: a reload or opening
|
|
25
|
-
// drops it too, a failed create keeps it).
|
|
20
|
+
// Beside it, the start page's ⚙ panel (start_settings.js): thinking, LLM
|
|
21
|
+
// context and review files, each a choice for the next new session only
|
|
22
|
+
// (sent with its create, then dropped; never remembered: a reload or opening
|
|
23
|
+
// a session drops it too, a failed create keeps it). What is set shows as
|
|
24
|
+
// override chips on the row, each opening the panel (a dot on ⚙ on a phone).
|
|
25
|
+
// And its 🧰 list (start_toolkit.js): the new session's toolkit, the same
|
|
26
|
+
// kind of choice (Auto, nothing sent, by default), with its own override
|
|
27
|
+
// chip and dot; hidden without GET /api/models' toolkits.
|
|
26
28
|
export const START_PICK_TITLE = "The model the new session starts on";
|
|
27
29
|
|
|
28
30
|
// Used by app.js. Elements: +pickEl+ (the picker's button), +panelEl+,
|
|
29
31
|
// +searchEl+, +listEl+, +composerEl+, +centerEl+ (model_picker.js'),
|
|
30
|
-
// +llmContextEl+ / +
|
|
32
|
+
// +settingsEl+ (⚙), +thinkingEl+ / +llmContextEl+ / +reviewEl+ (the override
|
|
33
|
+
// chips; no +reviewEl+: no review files), +toolkitBtnEl+ (🧰) and
|
|
34
|
+
// +toolkitEl+ (its override chip; both optional).
|
|
31
35
|
// Hooks: +selected()+ the open session's id or null; +state()+ the open
|
|
32
36
|
// session's fields (session_state.js: model, continues…); +running()+ a turn
|
|
33
37
|
// runs; +sessions()+ the list; +run(line)+ a session command from a control
|
|
34
38
|
// (resolves true once the worker took it); +focusPrompt()+; +toast(text)+;
|
|
35
39
|
// +listModels()+ GET /api/models (data.js). +win+: the page's unload events.
|
|
36
40
|
export function createStartPage({
|
|
37
|
-
pickEl, panelEl, searchEl, listEl, composerEl, centerEl, llmContextEl, thinkingEl, reviewEl,
|
|
41
|
+
pickEl, panelEl, searchEl, listEl, composerEl, centerEl, settingsEl, llmContextEl, thinkingEl, reviewEl = null,
|
|
42
|
+
toolkitBtnEl = null, toolkitEl = null,
|
|
38
43
|
selected, state, running, sessions, run, focusPrompt, toast, listModels,
|
|
39
44
|
doc = globalThis.document, win = globalThis.window,
|
|
40
45
|
}) {
|
|
@@ -43,6 +48,10 @@ export function createStartPage({
|
|
|
43
48
|
let newChatThinking = null;
|
|
44
49
|
// The files that make the new session a review session ([]: an ordinary one).
|
|
45
50
|
let newChatReview = [];
|
|
51
|
+
// The kit the new session starts on (null: Auto, nothing sent), and the
|
|
52
|
+
// models payload's toolkits (null: no 🧰).
|
|
53
|
+
let newChatToolkit = null;
|
|
54
|
+
let toolkits = null;
|
|
46
55
|
let startPickTitle = START_PICK_TITLE;
|
|
47
56
|
// The fresh link the picker shows (null: the start page, or a session it
|
|
48
57
|
// is hidden in), and a link pick's /model still in flight (a send waits).
|
|
@@ -57,65 +66,79 @@ export function createStartPage({
|
|
|
57
66
|
let pageLeaving = false;
|
|
58
67
|
|
|
59
68
|
const newChatLlmSummary = () => (selected() || pickEl.hidden ? null : modelPicker.chosenRow()?.llm_context || null);
|
|
60
|
-
const llmContextNewControl = createLlmContextControl({
|
|
61
|
-
el: llmContextEl,
|
|
62
|
-
summary: newChatLlmSummary,
|
|
63
|
-
kind: NEW_SESSION,
|
|
64
|
-
choice: () => newChatLlmContext,
|
|
65
|
-
choose: (next) => {
|
|
66
|
-
newChatLlmContext = next;
|
|
67
|
-
renderNewChatLlmContext();
|
|
68
|
-
focusPrompt();
|
|
69
|
-
},
|
|
70
|
-
id: "llmContextNewPopover",
|
|
71
|
-
anchor: "#llmContextNew",
|
|
72
|
-
});
|
|
73
69
|
const newChatThinkingSummary = () => (selected() || pickEl.hidden ? null : modelPicker.chosenRow()?.thinking || null);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
// A pick in the panel keeps the focus there (it stays open; the prompt's
|
|
71
|
+
// focus would raise a phone's keyboard behind the sheet).
|
|
72
|
+
const settings = createStartSettings({
|
|
73
|
+
el: settingsEl,
|
|
74
|
+
button: settingsEl,
|
|
75
|
+
openers: [settingsEl, thinkingEl, llmContextEl, reviewEl].filter(Boolean).map((e) => `#${e.id}`).join(", "),
|
|
76
|
+
thinking: {
|
|
77
|
+
summary: newChatThinkingSummary,
|
|
78
|
+
choice: () => newChatThinking,
|
|
79
|
+
choose: (next) => { newChatThinking = next; renderOverrides(); },
|
|
83
80
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
el: reviewEl,
|
|
89
|
-
choice: () => newChatReview,
|
|
90
|
-
choose: (next) => {
|
|
91
|
-
newChatReview = next;
|
|
92
|
-
renderNewChatReview();
|
|
93
|
-
focusPrompt();
|
|
81
|
+
llmContext: {
|
|
82
|
+
summary: newChatLlmSummary,
|
|
83
|
+
choice: () => newChatLlmContext,
|
|
84
|
+
choose: (next) => { newChatLlmContext = next; renderOverrides(); },
|
|
94
85
|
},
|
|
86
|
+
review: reviewEl ? {
|
|
87
|
+
choice: () => newChatReview,
|
|
88
|
+
choose: (next) => { newChatReview = next; renderOverrides(); },
|
|
89
|
+
} : null,
|
|
90
|
+
doc,
|
|
91
|
+
win,
|
|
92
|
+
});
|
|
93
|
+
thinkingEl.addEventListener("click", () => settings.toggle("main"));
|
|
94
|
+
llmContextEl.addEventListener("click", () => settings.toggle("main"));
|
|
95
|
+
reviewEl?.addEventListener("click", () => settings.toggle("review"));
|
|
96
|
+
const toolkitList = toolkitBtnEl ? createStartToolkit({
|
|
97
|
+
el: toolkitBtnEl,
|
|
98
|
+
button: toolkitBtnEl,
|
|
99
|
+
openers: [toolkitBtnEl, toolkitEl].filter(Boolean).map((e) => `#${e.id}`).join(", "),
|
|
100
|
+
toolkits: () => toolkits,
|
|
101
|
+
choice: () => newChatToolkit,
|
|
102
|
+
choose: (next) => { newChatToolkit = next; renderOverrides(); },
|
|
103
|
+
review: () => newChatReview.length > 0,
|
|
104
|
+
doc,
|
|
105
|
+
win,
|
|
95
106
|
}) : null;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
toolkitEl?.addEventListener("click", () => toolkitList?.toggle());
|
|
108
|
+
// The override chips (only what is set) and ⚙: its dot and label say
|
|
109
|
+
// what is set; hidden with nothing to show (no model values, no review).
|
|
110
|
+
function renderOverrides() {
|
|
111
|
+
const thinkingSummary = newChatThinkingSummary();
|
|
112
|
+
const llmContextSummary = newChatLlmSummary();
|
|
113
|
+
const over = overrides({ thinking: newChatThinking, llmContext: newChatLlmContext, review: newChatReview,
|
|
114
|
+
thinkingSummary, llmContextSummary });
|
|
115
|
+
const paint = (chipEl, chip) => {
|
|
116
|
+
if (!chipEl) return;
|
|
117
|
+
chipEl.hidden = !chip;
|
|
118
|
+
chipEl.textContent = chip?.text || "";
|
|
119
|
+
chipEl.title = chip?.title || "";
|
|
120
|
+
};
|
|
121
|
+
paint(thinkingEl, over.thinking);
|
|
122
|
+
paint(llmContextEl, over.llmContext);
|
|
123
|
+
paint(reviewEl, over.review);
|
|
124
|
+
const label = settingsLabel(over);
|
|
125
|
+
settingsEl.hidden = !(thinkingSummary || llmContextSummary || reviewEl || label.set);
|
|
126
|
+
settingsEl.classList.toggle("set", label.set);
|
|
127
|
+
settingsEl.title = label.label;
|
|
128
|
+
settingsEl.setAttribute("aria-label", label.label);
|
|
129
|
+
if (settingsEl.hidden) settings.close();
|
|
130
|
+
else settings.refresh();
|
|
131
|
+
if (!toolkitBtnEl) return;
|
|
132
|
+
// 🧰 needs the list; a kit picked before it went (a failed reload of
|
|
133
|
+
// the models) still shows as its chip, and is still sent.
|
|
134
|
+
const kit = toolkitLabel(newChatToolkit);
|
|
135
|
+
paint(toolkitEl, toolkitOverride(newChatToolkit));
|
|
136
|
+
toolkitBtnEl.hidden = !toolkits;
|
|
137
|
+
toolkitBtnEl.classList.toggle("set", kit.set);
|
|
138
|
+
toolkitBtnEl.title = kit.label;
|
|
139
|
+
toolkitBtnEl.setAttribute("aria-label", kit.label);
|
|
140
|
+
if (toolkitBtnEl.hidden) toolkitList.close();
|
|
141
|
+
else toolkitList.refresh();
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
const modelPicker = createModelPicker({
|
|
@@ -129,8 +152,8 @@ export function createStartPage({
|
|
|
129
152
|
onPick: (name) => {
|
|
130
153
|
if (selected()) pickForLink(name);
|
|
131
154
|
else {
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
renderOverrides();
|
|
156
|
+
syncStartPickLabel();
|
|
134
157
|
}
|
|
135
158
|
focusPrompt();
|
|
136
159
|
},
|
|
@@ -154,6 +177,12 @@ export function createStartPage({
|
|
|
154
177
|
pendingPickId = id;
|
|
155
178
|
pickRanEarly = false;
|
|
156
179
|
}
|
|
180
|
+
function syncStartPickLabel() {
|
|
181
|
+
const fullName = modelPicker.chosen() || "";
|
|
182
|
+
pickEl.title = fullName ? `${startPickTitle} · ${fullName}` : startPickTitle;
|
|
183
|
+
pickEl.setAttribute("aria-label", fullName
|
|
184
|
+
? `Model for the new session: ${fullName}` : "Model for the new session");
|
|
185
|
+
}
|
|
157
186
|
// The button's name, tooltip and label for what the page shows: a fresh
|
|
158
187
|
// link's model, else the start page's (whose choice stays as it is).
|
|
159
188
|
// Toggles the fresh-link class (the CSS shows the button in a session
|
|
@@ -179,8 +208,12 @@ export function createStartPage({
|
|
|
179
208
|
pickEl.title = "The model this link starts on";
|
|
180
209
|
pickEl.setAttribute("aria-label", "Model this link starts on");
|
|
181
210
|
} else {
|
|
182
|
-
|
|
183
|
-
|
|
211
|
+
if (open) {
|
|
212
|
+
pickEl.title = startPickTitle;
|
|
213
|
+
pickEl.setAttribute("aria-label", "Model for the new session");
|
|
214
|
+
} else {
|
|
215
|
+
syncStartPickLabel();
|
|
216
|
+
}
|
|
184
217
|
}
|
|
185
218
|
}
|
|
186
219
|
// A command_ran with a model (stream_handlers.js): while a pick is in
|
|
@@ -192,8 +225,8 @@ export function createStartPage({
|
|
|
192
225
|
// Leaving a session: the start page's remembered model again (no fetch).
|
|
193
226
|
function restoreStartPick() {
|
|
194
227
|
modelPicker.restore(readStored(MODEL_KEY));
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
syncStartPickLabel();
|
|
229
|
+
renderOverrides();
|
|
197
230
|
}
|
|
198
231
|
win.addEventListener("beforeunload", () => { pageLeaving = true; });
|
|
199
232
|
win.addEventListener("pagehide", () => { pageLeaving = true; });
|
|
@@ -206,53 +239,44 @@ export function createStartPage({
|
|
|
206
239
|
} catch (e) {
|
|
207
240
|
const text = modelsFailedText(e, { startPage: !selected(), asked, leaving: pageLeaving });
|
|
208
241
|
if (text) toast(text);
|
|
242
|
+
// ⚙ without the model's rows (its review files only), no 🧰.
|
|
243
|
+
toolkits = null;
|
|
244
|
+
renderOverrides();
|
|
209
245
|
return;
|
|
210
246
|
}
|
|
247
|
+
toolkits = payload?.toolkits?.kits?.length ? payload.toolkits : null;
|
|
211
248
|
const stored = readStored(MODEL_KEY);
|
|
212
|
-
if (!modelPicker.setModels(payload, stored)) return;
|
|
249
|
+
if (!modelPicker.setModels(payload, stored)) return renderOverrides();
|
|
213
250
|
startPickTitle = payload.warning ? `${START_PICK_TITLE} · ${payload.warning}` : START_PICK_TITLE;
|
|
214
251
|
pickEl.hidden = false;
|
|
215
252
|
// A fresh link open meanwhile shows its own model again, not the stored one.
|
|
216
253
|
syncModelPick();
|
|
217
|
-
|
|
218
|
-
renderNewChatThinking();
|
|
254
|
+
renderOverrides();
|
|
219
255
|
}
|
|
220
256
|
// The picker's choice for a create request; nothing (the server's default)
|
|
221
257
|
// while the list is not in.
|
|
222
258
|
function chosenModel() {
|
|
223
259
|
return pickEl.hidden ? undefined : modelPicker.chosen() || undefined;
|
|
224
260
|
}
|
|
225
|
-
// The create request's own choices (the
|
|
261
|
+
// The create request's own choices (the ⚙ panel's).
|
|
226
262
|
function newChoices() {
|
|
227
263
|
return { llm_context: choiceWords(newChatLlmContext), thinking: newChatThinking || undefined,
|
|
228
|
-
review: newChatReview.length ? { subjects: newChatReview } : undefined
|
|
264
|
+
review: newChatReview.length ? { subjects: newChatReview } : undefined,
|
|
265
|
+
toolkit: newChatToolkit || undefined };
|
|
229
266
|
}
|
|
230
267
|
// The choices were for that new session only: a create took them.
|
|
231
268
|
function clearChoices() {
|
|
232
269
|
newChatLlmContext = null;
|
|
233
|
-
renderNewChatLlmContext();
|
|
234
270
|
newChatThinking = null;
|
|
235
|
-
renderNewChatThinking();
|
|
236
271
|
newChatReview = [];
|
|
237
|
-
|
|
272
|
+
newChatToolkit = null;
|
|
273
|
+
renderOverrides();
|
|
238
274
|
}
|
|
239
275
|
// A session opened: the start page's choices were for a new session, they go.
|
|
240
276
|
function dropChoices() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
renderNewChatLlmContext();
|
|
245
|
-
}
|
|
246
|
-
thinkingNewControl.close();
|
|
247
|
-
if (newChatThinking) {
|
|
248
|
-
newChatThinking = null;
|
|
249
|
-
renderNewChatThinking();
|
|
250
|
-
}
|
|
251
|
-
reviewNewControl?.close();
|
|
252
|
-
if (newChatReview.length) {
|
|
253
|
-
newChatReview = [];
|
|
254
|
-
renderNewChatReview();
|
|
255
|
-
}
|
|
277
|
+
settings.close();
|
|
278
|
+
toolkitList?.close();
|
|
279
|
+
clearChoices();
|
|
256
280
|
}
|
|
257
281
|
|
|
258
282
|
return {
|
|
@@ -261,11 +285,19 @@ export function createStartPage({
|
|
|
261
285
|
pendingPick: () => pendingPick,
|
|
262
286
|
// The start page's review files ([]: the new session isn't a review).
|
|
263
287
|
reviewSubjects: () => newChatReview,
|
|
264
|
-
// Start refused them (400 invalid_review): said in the
|
|
265
|
-
// again. @return whether it was said there
|
|
288
|
+
// Start refused them (400 invalid_review): said in the ⚙ panel's review
|
|
289
|
+
// view, opened again. @return whether it was said there
|
|
266
290
|
reviewRefused: (message) => {
|
|
267
|
-
if (!
|
|
268
|
-
|
|
291
|
+
if (!reviewEl || !newChatReview.length) return false;
|
|
292
|
+
settings.refuse(message);
|
|
293
|
+
return true;
|
|
294
|
+
},
|
|
295
|
+
// Start refused the picked kit (400 toolkit_refused): said in the 🧰
|
|
296
|
+
// list, opened again. Without a pick it is config.yml's default that
|
|
297
|
+
// is broken: @return false (the caller's alert).
|
|
298
|
+
toolkitRefused: (message) => {
|
|
299
|
+
if (!toolkitList || !newChatToolkit || !toolkits) return false;
|
|
300
|
+
toolkitList.refuse(message);
|
|
269
301
|
return true;
|
|
270
302
|
},
|
|
271
303
|
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// The start page's ⚙ panel: the new session's rarely used settings in one
|
|
2
|
+
// place (thinking, LLM context, review files), each a choice for the next
|
|
3
|
+
// create only (start_page.js keeps them; never remembered). A pick applies
|
|
4
|
+
// at once and the panel stays open; a typed budget has its own Set. Rows
|
|
5
|
+
// the picked model has no values for are hidden, but a choice already made
|
|
6
|
+
// is still sent: the panel says so and offers to clear it. The review files
|
|
7
|
+
// are a view of their own (‹ back to the settings).
|
|
8
|
+
//
|
|
9
|
+
// Beside ⚙ on the row, override chips name what is set (overrides(): "think
|
|
10
|
+
// high", "ctx stale", "review: README.md"); on a phone ⚙ carries a dot
|
|
11
|
+
// instead (settingsLabel()).
|
|
12
|
+
|
|
13
|
+
import { createChipPopover } from "./chip_popover.js";
|
|
14
|
+
import { APPLIES, choiceWords, mergeChoice, ownValues, strategyValue } from "./llm_context_chip.js";
|
|
15
|
+
import { readSubjects, reviewChipModel, reviewFormTemplate } from "./review_start.js";
|
|
16
|
+
import { LEVELS } from "./thinking_chip.js";
|
|
17
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
18
|
+
|
|
19
|
+
export const DEFAULT = "default";
|
|
20
|
+
export const NOTE = "For this new session only.";
|
|
21
|
+
export const PANEL_LABEL = "Session settings";
|
|
22
|
+
const NO_THINKING = "the picked model has no thinking setting; it is sent anyway";
|
|
23
|
+
const NO_LLM_CONTEXT = "the picked model has no LLM context values; it is sent anyway";
|
|
24
|
+
|
|
25
|
+
const STRATEGIES = [
|
|
26
|
+
{ value: "none", label: "none" },
|
|
27
|
+
{ value: "stale", label: "stale" },
|
|
28
|
+
{ value: "stale,forget", label: "stale + forget", title: "stale + forget (experimental)" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// The first segment: follow the model, its value in brackets ("default
|
|
32
|
+
// (high)"; a model with no level of its own, "default" alone).
|
|
33
|
+
const followSegment = (value) => ({ value: DEFAULT, label: value === DEFAULT ? DEFAULT : `${DEFAULT} (${value})`,
|
|
34
|
+
title: `Follow the model (${value})` });
|
|
35
|
+
|
|
36
|
+
// The Thinking row's segments: the model's own level first ("default
|
|
37
|
+
// (high)"), then the levels; the pressed one is the choice. null without
|
|
38
|
+
// the model's thinking (the row is hidden).
|
|
39
|
+
export function thinkingSegments(summary, choice) {
|
|
40
|
+
if (!summary?.level) return null;
|
|
41
|
+
const pressed = choice || DEFAULT;
|
|
42
|
+
return [followSegment(summary.level),
|
|
43
|
+
...LEVELS.map((level) => ({ value: level, label: level }))].map((s) => ({ ...s, pressed: s.value === pressed }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The Context row's segments over the strategies (a choice that isn't one
|
|
47
|
+
// of them, a newer chi's layer, is added); null without the model's
|
|
48
|
+
// llm_context (the row is hidden).
|
|
49
|
+
export function contextSegments(summary, choice) {
|
|
50
|
+
if (!summary?.strategy) return null;
|
|
51
|
+
const pressed = Array.isArray(choice?.strategy) ? strategyValue(choice.strategy) : DEFAULT;
|
|
52
|
+
const list = STRATEGIES.some((s) => s.value === pressed) || pressed === DEFAULT ? STRATEGIES
|
|
53
|
+
: [...STRATEGIES, { value: pressed, label: pressed }];
|
|
54
|
+
return [followSegment(summary.strategy), ...list].map((s) => ({ ...s, pressed: s.value === pressed }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The context choice in a few words ("stale · budget 64000"), or null.
|
|
58
|
+
export function contextWords(choice) {
|
|
59
|
+
const words = choiceWords(choice);
|
|
60
|
+
if (!words) return null;
|
|
61
|
+
return [words.strategy, words.apply && `apply ${words.apply}`, words.budget && `budget ${words.budget}`]
|
|
62
|
+
.filter(Boolean).join(" · ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The row's override chips: what is set for the new session, each
|
|
66
|
+
// {text, title} or null. A choice the picked model shows no row for (its
|
|
67
|
+
// summary is null) stays, its title saying it is sent anyway.
|
|
68
|
+
export function overrides({ thinking = null, llmContext = null, review = [], thinkingSummary = null,
|
|
69
|
+
llmContextSummary = null } = {}) {
|
|
70
|
+
const ctx = contextWords(llmContext);
|
|
71
|
+
return {
|
|
72
|
+
thinking: thinking ? {
|
|
73
|
+
text: `think ${thinking}`,
|
|
74
|
+
title: thinkingSummary
|
|
75
|
+
? `Thinking: ${thinking} for the new session (the model's: ${thinkingSummary.level}). Click to change it.`
|
|
76
|
+
: `Thinking: ${thinking} for the new session: ${NO_THINKING}. Click to change it.`,
|
|
77
|
+
} : null,
|
|
78
|
+
llmContext: ctx ? {
|
|
79
|
+
text: `ctx ${ctx}`,
|
|
80
|
+
title: llmContextSummary
|
|
81
|
+
? `LLM context: ${ctx} for the new session (the model's: ${llmContextSummary.strategy}). Click to change it.`
|
|
82
|
+
: `LLM context: ${ctx} for the new session: ${NO_LLM_CONTEXT}. Click to change it.`,
|
|
83
|
+
} : null,
|
|
84
|
+
review: review?.length ? reviewChipModel(review) : null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ⚙'s state: whether anything is set (its dot on a phone) and its tooltip
|
|
89
|
+
// and accessible name, which list what is.
|
|
90
|
+
export function settingsLabel(over) {
|
|
91
|
+
const set = [over.thinking, over.llmContext, over.review].filter(Boolean);
|
|
92
|
+
return { set: set.length > 0, label: set.length ? `${PANEL_LABEL}: ${set.map((o) => o.text).join(", ")}` : PANEL_LABEL };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const segmentsTemplate = (name, label, segments, act) => html`<div class="ss-row"><span class="ss-label"
|
|
96
|
+
id=${`ss-${name}-label`}>${label}</span><div class="ss-seg" role="group" aria-labelledby=${`ss-${name}-label`}>${
|
|
97
|
+
segments.map((s) => html`<button type="button" data-act=${`${act}:${s.value}`} aria-pressed=${s.pressed ? "true" : "false"}
|
|
98
|
+
title=${s.title || nothing}>${s.label}</button>`)}</div></div>`;
|
|
99
|
+
|
|
100
|
+
const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
|
|
101
|
+
|
|
102
|
+
// The disclosure's fields: the apply rule and the budget, with their own
|
|
103
|
+
// Set (a typed budget needs a commit) and mergeChoice's error under them.
|
|
104
|
+
function contextMoreTemplate(summary, choice, error) {
|
|
105
|
+
const own = ownValues({ own: choice });
|
|
106
|
+
const budget = summary.budget_tokens ? String(summary.budget_tokens) : "off";
|
|
107
|
+
return html`<form class="ss-ctx-form"><label class="llmctx-field"><span>Apply</span><span class="select-wrap"><select
|
|
108
|
+
name="apply">${option(DEFAULT, `${DEFAULT} (${summary.apply})`, own.apply)}${APPLIES.map((a) => option(a, a, own.apply))}</select
|
|
109
|
+
></span></label><label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric"
|
|
110
|
+
autocomplete="off" placeholder=${`${DEFAULT} (${budget}): 64000, 64k or off`} .value=${own.budget}></label
|
|
111
|
+
><div class="ctx-pop-error" role="alert" ?hidden=${!error}>${error || ""}</div
|
|
112
|
+
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A choice whose row is hidden (the picked model has no values for it).
|
|
116
|
+
const sentAnywayTemplate = (text, act) => html`<div class="ss-anyway ctx-pop-meta">${text}
|
|
117
|
+
<button type="button" class="ss-link" data-act=${act}>Clear</button></div>`;
|
|
118
|
+
|
|
119
|
+
// The main view. +state+: {thinking: {summary, choice}, llmContext: {summary,
|
|
120
|
+
// choice}, review: subjects or null (no review row), more: the disclosure
|
|
121
|
+
// open, error: the context Set's}.
|
|
122
|
+
export function panelTemplate({ thinking, llmContext, review = null, more = false, error = null }) {
|
|
123
|
+
const think = thinkingSegments(thinking.summary, thinking.choice);
|
|
124
|
+
const ctx = contextSegments(llmContext.summary, llmContext.choice);
|
|
125
|
+
const ctxAnyway = !ctx && contextWords(llmContext.choice);
|
|
126
|
+
const reviewChip = review?.length ? reviewChipModel(review) : null;
|
|
127
|
+
return html`<div class="ctx-pop-head"><strong>New session</strong
|
|
128
|
+
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
129
|
+
>${think ? segmentsTemplate("think", "Thinking", think, "think") : nothing
|
|
130
|
+
}${!think && thinking.choice ? sentAnywayTemplate(`Thinking ${thinking.choice}: ${NO_THINKING}.`, "think-clear") : nothing
|
|
131
|
+
}${ctx ? html`${segmentsTemplate("ctx", "Context", ctx, "ctx")}<div class="ss-more"><button type="button" class="ss-link"
|
|
132
|
+
data-act="ctx-more" aria-expanded=${more ? "true" : "false"}>Apply and budget ${more ? "▾" : "▸"}</button></div
|
|
133
|
+
>${more ? contextMoreTemplate(llmContext.summary, llmContext.choice, error) : nothing}` : nothing
|
|
134
|
+
}${ctxAnyway ? sentAnywayTemplate(`Context ${ctxAnyway}: ${NO_LLM_CONTEXT}.`, "ctx-clear") : nothing
|
|
135
|
+
}${review ? html`${think || ctx ? html`<hr class="ss-rule">` : nothing}<div class="ss-review">${reviewChip
|
|
136
|
+
? html`<span class="ss-review-set" title=${reviewChip.title}>Review: ${reviewChip.text.replace(/^review: /, "")}</span
|
|
137
|
+
><button type="button" class="ss-link" data-act="review">Edit</button><button type="button" class="ss-link"
|
|
138
|
+
data-act="review-clear">Remove</button>`
|
|
139
|
+
: html`<button type="button" class="ss-link" data-act="review" title=${reviewChipModel([]).title}>Review files…</button>`}</div>`
|
|
140
|
+
: nothing
|
|
141
|
+
}<div class="ctx-pop-meta">${NOTE}</div>`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The panel on chip_popover.js' shell, a bottom sheet on a phone. +el+:
|
|
145
|
+
// where ⚙ is (its clicks are delegated); +button+: ⚙ (the panel opens at it,
|
|
146
|
+
// and focus goes back to it whichever opener was clicked); +openers+: the
|
|
147
|
+
// selector of every element that opens it (⚙ and the override chips: a click
|
|
148
|
+
// there is not a click outside). +thinking+ / +llmContext+: {summary(),
|
|
149
|
+
// choice(), choose(next)}; +review+: {choice(), choose(subjects)}, or null
|
|
150
|
+
// (no review row).
|
|
151
|
+
// @return {open(view), toggle(view), close, refresh, refuse(message), isOpen}
|
|
152
|
+
export function createStartSettings({ el, button, openers = `#${button.id}`, thinking, llmContext, review = null,
|
|
153
|
+
doc = document, win = window }) {
|
|
154
|
+
// "main" or "review"; the disclosure; the context Set's error; the review
|
|
155
|
+
// form's error and text as typed.
|
|
156
|
+
let view = "main";
|
|
157
|
+
let more = false;
|
|
158
|
+
let error = null;
|
|
159
|
+
let reviewError = null;
|
|
160
|
+
let reviewText = null;
|
|
161
|
+
|
|
162
|
+
function draw() {
|
|
163
|
+
if (view === "review") {
|
|
164
|
+
render(reviewFormTemplate(review.choice(), { error: reviewError, text: reviewText, back: true }), shell.pop);
|
|
165
|
+
} else {
|
|
166
|
+
render(panelTemplate({
|
|
167
|
+
thinking: { summary: thinking.summary(), choice: thinking.choice() },
|
|
168
|
+
llmContext: { summary: llmContext.summary(), choice: llmContext.choice() },
|
|
169
|
+
review: review ? review.choice() : null, more, error,
|
|
170
|
+
}), shell.pop);
|
|
171
|
+
}
|
|
172
|
+
shell.replace();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Focus inside on open (the popover sits at the end of <body>, so Tab
|
|
176
|
+
// from ⚙ wouldn't reach it): the pressed segment of the first row, else
|
|
177
|
+
// the first button; the review view's text field.
|
|
178
|
+
function focusInside() {
|
|
179
|
+
const pop = shell.pop;
|
|
180
|
+
const at = view === "review" ? pop.querySelector("textarea")
|
|
181
|
+
: pop.querySelector(".ss-seg [aria-pressed=true]") || pop.querySelector(".ss-link, .ss-seg button");
|
|
182
|
+
at?.focus();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function switchTo(next) {
|
|
186
|
+
view = next;
|
|
187
|
+
draw();
|
|
188
|
+
if (next === "review") focusInside();
|
|
189
|
+
else shell.pop.querySelector("[data-act=review]")?.focus();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function pickContext(changes) {
|
|
193
|
+
const initial = ownValues({ own: llmContext.choice() });
|
|
194
|
+
let next;
|
|
195
|
+
try {
|
|
196
|
+
next = mergeChoice(llmContext.choice(), { ...initial, ...changes }, initial);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
error = err.message;
|
|
199
|
+
return draw();
|
|
200
|
+
}
|
|
201
|
+
error = null;
|
|
202
|
+
llmContext.choose(next);
|
|
203
|
+
draw();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function act(name) {
|
|
207
|
+
const [what, value] = name.split(/:(.*)/s);
|
|
208
|
+
if (what === "think") thinking.choose(value === DEFAULT ? null : value);
|
|
209
|
+
else if (what === "think-clear") thinking.choose(null);
|
|
210
|
+
else if (what === "ctx") return pickContext({ strategy: value });
|
|
211
|
+
else if (what === "ctx-clear") llmContext.choose(null);
|
|
212
|
+
else if (what === "ctx-more") more = !more;
|
|
213
|
+
else if (what === "review") return switchTo("review");
|
|
214
|
+
else if (what === "back") {
|
|
215
|
+
reviewError = null;
|
|
216
|
+
reviewText = null;
|
|
217
|
+
return switchTo("main");
|
|
218
|
+
} else if (what === "review-clear" || what === "clear") {
|
|
219
|
+
review.choose([]);
|
|
220
|
+
reviewError = null;
|
|
221
|
+
reviewText = null;
|
|
222
|
+
if (view === "review") return switchTo("main");
|
|
223
|
+
} else return;
|
|
224
|
+
draw();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function submit(form) {
|
|
228
|
+
if (form.classList.contains("ss-ctx-form")) {
|
|
229
|
+
return pickContext({ apply: form.elements.apply.value, budget: form.elements.budget.value });
|
|
230
|
+
}
|
|
231
|
+
const typed = form.elements.subjects?.value || "";
|
|
232
|
+
const read = readSubjects(typed);
|
|
233
|
+
if (read.error) {
|
|
234
|
+
reviewError = read.error;
|
|
235
|
+
reviewText = typed;
|
|
236
|
+
return draw();
|
|
237
|
+
}
|
|
238
|
+
review.choose(read.subjects);
|
|
239
|
+
reviewError = null;
|
|
240
|
+
reviewText = null;
|
|
241
|
+
switchTo("main");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const shell = createChipPopover({
|
|
245
|
+
id: "startSettingsPopover", className: "ctx-popover start-settings", width: 440, el, anchor: `#${button.id}`, openers,
|
|
246
|
+
summary: () => ({}), sheet: true, label: PANEL_LABEL, doc, win,
|
|
247
|
+
show: (_data, opening) => {
|
|
248
|
+
if (opening) {
|
|
249
|
+
const choice = llmContext.choice();
|
|
250
|
+
more = !!(choice?.apply || typeof choice?.budget_tokens === "number");
|
|
251
|
+
}
|
|
252
|
+
draw();
|
|
253
|
+
},
|
|
254
|
+
onOpen: focusInside,
|
|
255
|
+
onClose: () => {
|
|
256
|
+
view = "main";
|
|
257
|
+
error = null;
|
|
258
|
+
reviewError = null;
|
|
259
|
+
reviewText = null;
|
|
260
|
+
},
|
|
261
|
+
onSubmit: submit,
|
|
262
|
+
onAct: act,
|
|
263
|
+
});
|
|
264
|
+
// An edit makes the context Set's error stale.
|
|
265
|
+
shell.pop.addEventListener("input", (e) => {
|
|
266
|
+
if (error && e.target.closest(".ss-ctx-form")) {
|
|
267
|
+
error = null;
|
|
268
|
+
draw();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function open(at = "main") {
|
|
273
|
+
view = review ? at : "main";
|
|
274
|
+
if (shell.isOpen()) {
|
|
275
|
+
draw();
|
|
276
|
+
focusInside();
|
|
277
|
+
} else {
|
|
278
|
+
shell.open(button);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
open,
|
|
284
|
+
// An override chip's click: opens the panel (on +at+'s view), or closes it.
|
|
285
|
+
toggle: (at = "main") => (shell.isOpen() ? shell.close() : open(at)),
|
|
286
|
+
close: () => shell.close(),
|
|
287
|
+
isOpen: () => shell.isOpen(),
|
|
288
|
+
// The picked model changed (its default labels, which rows show).
|
|
289
|
+
refresh: () => { if (shell.isOpen()) draw(); },
|
|
290
|
+
// Start refused the review files (400 invalid_review): said in the
|
|
291
|
+
// review view, opened at the files chosen.
|
|
292
|
+
refuse: (message) => {
|
|
293
|
+
reviewError = message;
|
|
294
|
+
reviewText = null;
|
|
295
|
+
open("review");
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|