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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The settings panel's rows, drawn by the start page's ⚙ panel
|
|
2
|
+
// (start_settings.js) over a new session's choices and by the session bar's
|
|
3
|
+
// (settings_chip.js) over the open session's own: rows of segments
|
|
4
|
+
// (Thinking, Context), the Context row's Apply and budget disclosure, a
|
|
5
|
+
// line for a choice whose row is hidden, the review files' line. The caller
|
|
6
|
+
// passes the rows it draws (settingsPanelTemplate's +rows+) and applies the
|
|
7
|
+
// picks itself; the segments' data-act ("think:low", "ctx:stale",
|
|
8
|
+
// "ctx-more") is the same for every caller.
|
|
9
|
+
|
|
10
|
+
import { APPLIES, choiceWords, ownValues, strategyValue } from "./llm_context_chip.js";
|
|
11
|
+
import { DEFAULT, followSegment, segments } from "./level_chip.js";
|
|
12
|
+
import { reviewChipModel } from "./review_start.js";
|
|
13
|
+
import { CONFIG as THINKING } from "./thinking_chip.js";
|
|
14
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
15
|
+
|
|
16
|
+
export { DEFAULT };
|
|
17
|
+
|
|
18
|
+
const STRATEGIES = [
|
|
19
|
+
{ value: "none", label: "none" },
|
|
20
|
+
{ value: "stale", label: "stale" },
|
|
21
|
+
{ value: "stale,forget", label: "stale + forget", title: "stale + forget (experimental)" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// The Thinking row's segments (level_chip.js segments over thinking_chip.js'
|
|
25
|
+
// levels): the model's own level first ("default (high)"; "default" while
|
|
26
|
+
// the session has its own), then the levels;
|
|
27
|
+
// the pressed one is the choice. null without the model's thinking (the row
|
|
28
|
+
// is hidden).
|
|
29
|
+
export function thinkingSegments(summary, choice) {
|
|
30
|
+
return segments(THINKING, summary, choice);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Whether the summary's +field+ (strategy, apply, budget) is the session's
|
|
34
|
+
// own: the value in force is then not the model's, so a default label
|
|
35
|
+
// doesn't name it.
|
|
36
|
+
const ownField = (summary, field) => summary?.[`${field}_source`] === "session";
|
|
37
|
+
|
|
38
|
+
// The Context row's segments over the strategies (a choice that isn't one
|
|
39
|
+
// of them, a newer chi's layer, is added); null without the model's
|
|
40
|
+
// llm_context (the row is hidden).
|
|
41
|
+
export function contextSegments(summary, choice) {
|
|
42
|
+
if (!summary?.strategy) return null;
|
|
43
|
+
const pressed = Array.isArray(choice?.strategy) ? strategyValue(choice.strategy) : DEFAULT;
|
|
44
|
+
const list = STRATEGIES.some((s) => s.value === pressed) || pressed === DEFAULT ? STRATEGIES
|
|
45
|
+
: [...STRATEGIES, { value: pressed, label: pressed }];
|
|
46
|
+
return [followSegment(summary.strategy, ownField(summary, "strategy")), ...list]
|
|
47
|
+
.map((s) => ({ ...s, pressed: s.value === pressed }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The context choice in a few words ("stale · budget 64000"), or null.
|
|
51
|
+
export function contextWords(choice) {
|
|
52
|
+
const words = choiceWords(choice);
|
|
53
|
+
if (!words) return null;
|
|
54
|
+
return [words.strategy, words.apply && `apply ${words.apply}`, words.budget && `budget ${words.budget}`]
|
|
55
|
+
.filter(Boolean).join(" · ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const segmentsTemplate = (name, label, segs, act) => html`<div class="sset-row"><span class="sset-label"
|
|
59
|
+
id=${`sset-${name}-label`}>${label}</span><div class="sset-seg" role="group" aria-labelledby=${`sset-${name}-label`}>${
|
|
60
|
+
segs.map((s) => html`<button type="button" data-act=${`${act}:${s.value}`} aria-pressed=${s.pressed ? "true" : "false"}
|
|
61
|
+
title=${s.title || nothing}>${s.label}</button>`)}</div></div>`;
|
|
62
|
+
|
|
63
|
+
const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
|
|
64
|
+
|
|
65
|
+
// The disclosure's fields: the apply rule and the budget, with their own
|
|
66
|
+
// Set (a typed budget needs a commit) and mergeChoice's error under them.
|
|
67
|
+
// Each default names the model's value, unless the session set its own
|
|
68
|
+
// (the summary's value is the session's then).
|
|
69
|
+
export function contextMoreTemplate(summary, choice, error) {
|
|
70
|
+
const own = ownValues({ own: choice });
|
|
71
|
+
const budget = summary.budget_tokens ? String(summary.budget_tokens) : "off";
|
|
72
|
+
const apply = ownField(summary, "apply") ? DEFAULT : `${DEFAULT} (${summary.apply})`;
|
|
73
|
+
const budgetDefault = ownField(summary, "budget") ? DEFAULT : `${DEFAULT} (${budget})`;
|
|
74
|
+
return html`<form class="sset-ctx-form"><label class="llmctx-field"><span>Apply</span><span class="select-wrap"><select
|
|
75
|
+
name="apply">${option(DEFAULT, apply, own.apply)}${APPLIES.map((a) => option(a, a, own.apply))}</select
|
|
76
|
+
></span></label><label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric"
|
|
77
|
+
autocomplete="off" placeholder=${`${budgetDefault}: 64000, 64k or off`} .value=${own.budget}></label
|
|
78
|
+
><div class="ctx-pop-error" role="alert" ?hidden=${!error}>${error || ""}</div
|
|
79
|
+
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A choice whose row is hidden (the picked model has no values for it).
|
|
83
|
+
export const sentAnywayTemplate = (text, act) => html`<div class="sset-anyway ctx-pop-meta">${text}
|
|
84
|
+
<button type="button" class="sset-link" data-act=${act}>Clear</button></div>`;
|
|
85
|
+
|
|
86
|
+
// One of the panel's rows, null drawing nothing:
|
|
87
|
+
// {name, label, segments, act}: a row of segments (segmentsTemplate);
|
|
88
|
+
// with +more+ ({open, summary, choice, error}) the Apply and budget
|
|
89
|
+
// disclosure under it, its fields while open (contextMoreTemplate)
|
|
90
|
+
// {anyway, act}: the line for a choice whose row is hidden, its Clear
|
|
91
|
+
// button's data-act (sentAnywayTemplate)
|
|
92
|
+
function rowTemplate(row) {
|
|
93
|
+
if (!row) return nothing;
|
|
94
|
+
if (row.anyway) return sentAnywayTemplate(row.anyway, row.act);
|
|
95
|
+
const { more } = row;
|
|
96
|
+
return html`${segmentsTemplate(row.name, row.label, row.segments, row.act)}${more ? html`<div class="sset-ctx-more"
|
|
97
|
+
><button type="button" class="sset-link" data-act="ctx-more" aria-expanded=${more.open ? "true" : "false"}>Apply and budget ${
|
|
98
|
+
more.open ? "▾" : "▸"}</button></div>${more.open ? contextMoreTemplate(more.summary, more.choice, more.error) : nothing}`
|
|
99
|
+
: nothing}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The review files' line: the files set (Edit, Remove), else Review files….
|
|
103
|
+
function reviewTemplate(review, ruled) {
|
|
104
|
+
const chip = review.length ? reviewChipModel(review) : null;
|
|
105
|
+
return html`${ruled ? html`<hr class="sset-rule">` : nothing}<div class="sset-review">${chip
|
|
106
|
+
? html`<span class="sset-review-set" title=${chip.title}>Review: ${chip.text.replace(/^review: /, "")}</span
|
|
107
|
+
><button type="button" class="sset-link" data-act="review">Edit</button><button type="button" class="sset-link"
|
|
108
|
+
data-act="review-clear">Remove</button>`
|
|
109
|
+
: html`<button type="button" class="sset-link" data-act="review" title=${reviewChipModel([]).title}>Review files…</button>`}</div>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The panel: its heading (+kind+ beside it, or none) and close button, the
|
|
113
|
+
// +rows+ (rowTemplate's), the review files' line when +review+ is a list
|
|
114
|
+
// (null: no review line; a rule above it when a row of segments is drawn),
|
|
115
|
+
// and the +note+.
|
|
116
|
+
export function settingsPanelTemplate({ heading, kind = null, rows = [], review = null, note }) {
|
|
117
|
+
const segmented = rows.some((row) => row?.segments);
|
|
118
|
+
return html`<div class="ctx-pop-head"><strong>${heading}</strong>${kind ? html`<span class="ctx-pop-kind">${kind}</span>` : nothing
|
|
119
|
+
}<button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
120
|
+
>${rows.map(rowTemplate)}${review ? reviewTemplate(review, segmented) : nothing}<div class="ctx-pop-meta">${note}</div>`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// A `!cmd`'s saved user message: the worker appends "!(<command>)\n<output>"
|
|
2
|
+
// for the next turn (session_commands.rb, no turn id, no time). The history
|
|
3
|
+
// draws it as its command bubble (turn_html.js) and the reload's turns
|
|
4
|
+
// leave it out (timing.js turnGroups: a group of its own, no steps, no
|
|
5
|
+
// timing). Pure, no imports: timing.js and turn_html.js both read it, and
|
|
6
|
+
// turn_html.js imports timing.js. A module of its own: static JS is cached
|
|
7
|
+
// at unversioned URLs, and a new export on an old module would blank the
|
|
8
|
+
// page.
|
|
9
|
+
|
|
10
|
+
// Its command line and its output, or null when the content is any other
|
|
11
|
+
// message. The command sits on the first line inside `!(` `)`; the output
|
|
12
|
+
// is everything after that first newline. A message that merely starts
|
|
13
|
+
// with "!" or "!(" without the newline shape is a plain message.
|
|
14
|
+
export function shellCommandMessage(content) {
|
|
15
|
+
const text = String(content ?? "");
|
|
16
|
+
const match = text.match(/^!\((.*)\)\n([\s\S]*)$/);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
return { line: `!${match[1]}`, text: match[2] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A history item that is a `!cmd`'s record, not a prompt: a user message of
|
|
22
|
+
// that shape with no turn id (a prompt saves its turn's; session_chain.rb
|
|
23
|
+
// reads the record the same way).
|
|
24
|
+
export function isShellCommandItem(item) {
|
|
25
|
+
return item?.role === "user" && !item.turn_id && shellCommandMessage(item.content) !== null;
|
|
26
|
+
}
|
|
@@ -11,57 +11,21 @@
|
|
|
11
11
|
// instead (settingsLabel()).
|
|
12
12
|
|
|
13
13
|
import { createChipPopover, SHEET_TALL } from "./chip_popover.js";
|
|
14
|
-
import {
|
|
14
|
+
import { mergeChoice, ownValues } from "./llm_context_chip.js";
|
|
15
15
|
import { readSubjects, reviewChipModel, reviewFormTemplate } from "./review_start.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import {
|
|
17
|
+
contextSegments, contextWords, DEFAULT, settingsPanelTemplate, thinkingSegments,
|
|
18
|
+
} from "./settings_panel.js";
|
|
19
|
+
import { render } from "./vendor/lit-html.js";
|
|
20
|
+
|
|
21
|
+
// The panel's rows (settings_panel.js), named here too: the start page and
|
|
22
|
+
// its specs read them from this module.
|
|
23
|
+
export { contextSegments, contextWords, DEFAULT, thinkingSegments };
|
|
20
24
|
export const NOTE = "For this new session only.";
|
|
21
25
|
export const PANEL_LABEL = "Session settings";
|
|
22
26
|
const NO_THINKING = "the picked model has no thinking setting; it is sent anyway";
|
|
23
27
|
const NO_LLM_CONTEXT = "the picked model has no LLM context values; it is sent anyway";
|
|
24
28
|
|
|
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
29
|
// The row's override chips: what is set for the new session, each
|
|
66
30
|
// {text, title} or null. A choice the picked model shows no row for (its
|
|
67
31
|
// summary is null) stays, its title saying it is sent anyway.
|
|
@@ -92,53 +56,26 @@ export function settingsLabel(over) {
|
|
|
92
56
|
return { set: set.length > 0, label: set.length ? `${PANEL_LABEL}: ${set.map((o) => o.text).join(", ")}` : PANEL_LABEL };
|
|
93
57
|
}
|
|
94
58
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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="sset-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="sset-anyway ctx-pop-meta">${text}
|
|
117
|
-
<button type="button" class="sset-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}.
|
|
59
|
+
// The main view (settings_panel.js settingsPanelTemplate over the new
|
|
60
|
+
// session's choices). +state+: {thinking: {summary, choice}, llmContext:
|
|
61
|
+
// {summary, choice}, review: subjects or null (no review row), more: the
|
|
62
|
+
// disclosure open, error: the context Set's}.
|
|
122
63
|
export function panelTemplate({ thinking, llmContext, review = null, more = false, error = null }) {
|
|
123
64
|
const think = thinkingSegments(thinking.summary, thinking.choice);
|
|
124
65
|
const ctx = contextSegments(llmContext.summary, llmContext.choice);
|
|
125
66
|
const ctxAnyway = !ctx && contextWords(llmContext.choice);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
data-act="review-clear">Remove</button>`
|
|
139
|
-
: html`<button type="button" class="sset-link" data-act="review" title=${reviewChipModel([]).title}>Review files…</button>`}</div>`
|
|
140
|
-
: nothing
|
|
141
|
-
}<div class="ctx-pop-meta">${NOTE}</div>`;
|
|
67
|
+
return settingsPanelTemplate({
|
|
68
|
+
heading: "New session",
|
|
69
|
+
rows: [
|
|
70
|
+
think ? { name: "think", label: "Thinking", segments: think, act: "think" } : null,
|
|
71
|
+
!think && thinking.choice ? { anyway: `Thinking ${thinking.choice}: ${NO_THINKING}.`, act: "think-clear" } : null,
|
|
72
|
+
ctx ? { name: "ctx", label: "Context", segments: ctx, act: "ctx",
|
|
73
|
+
more: { open: more, summary: llmContext.summary, choice: llmContext.choice, error } } : null,
|
|
74
|
+
ctxAnyway ? { anyway: `Context ${ctxAnyway}: ${NO_LLM_CONTEXT}.`, act: "ctx-clear" } : null,
|
|
75
|
+
],
|
|
76
|
+
review,
|
|
77
|
+
note: NOTE,
|
|
78
|
+
});
|
|
142
79
|
}
|
|
143
80
|
|
|
144
81
|
// The panel on chip_popover.js' shell, a bottom sheet on a phone. +el+:
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// it came from are the session's thinking (SettingExplained#summary:
|
|
6
|
-
// {level, source, own}; the worker's snapshot, a command_ran, or the
|
|
7
|
-
// server's own reading of the session file). A config on level_chip.js.
|
|
8
|
-
//
|
|
9
|
-
// The start page's new session sets its level in the ⚙ panel instead
|
|
10
|
-
// (start_settings.js, over LEVELS).
|
|
1
|
+
// Thinking as a level setting (level_chip.js): the levels the settings
|
|
2
|
+
// panels' Thinking row offers (settings_panel.js; the start page's ⚙ panel,
|
|
3
|
+
// the session bar's, settings_chip.js) and the /thinking line a session's
|
|
4
|
+
// pick runs in its worker.
|
|
11
5
|
|
|
12
6
|
import * as level from "./level_chip.js";
|
|
13
|
-
import { html, nothing } from "./vendor/lit-html.js";
|
|
14
7
|
|
|
15
8
|
export const LEVELS = ["off", "low", "medium", "high"];
|
|
16
9
|
|
|
@@ -19,42 +12,11 @@ export const CONFIG = Object.freeze({
|
|
|
19
12
|
prefix: "think",
|
|
20
13
|
command: "/thinking",
|
|
21
14
|
levels: LEVELS,
|
|
22
|
-
|
|
23
|
-
formClass: "think-form",
|
|
24
|
-
popoverClass: "think-popover",
|
|
25
|
-
nowClass: "think-now",
|
|
26
|
-
when: () => "From the next turn's start. On Gemma the whole prompt is read again; a hosted API reads the whole conversation again.",
|
|
15
|
+
cost: "A thinking change: on Gemma the whole prompt is read again; a hosted API reads the whole conversation again.",
|
|
27
16
|
});
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// The chip: its text, tooltip and whether the session set its own; null
|
|
32
|
-
// without a summary (an older worker). The default with nothing set
|
|
33
|
-
// anywhere shows too ("think default"): the chip is where the level is set.
|
|
34
|
-
export function chipModel(summary) {
|
|
35
|
-
return level.chipModel(CONFIG, summary);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
39
|
-
// without a summary.
|
|
40
|
-
export function chipTemplate(summary) {
|
|
41
|
-
const chip = chipModel(summary);
|
|
42
|
-
if (!chip) return nothing;
|
|
43
|
-
return html`<button type="button" class=${`model think-chip${chip.own ? " own" : ""}`} title=${chip.title} data-think-chip aria-haspopup="dialog">${chip.text}</button>`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// The /thinking line for a select value, with --yes (level_chip.js
|
|
47
|
-
// commandLine); null when it is what the form opened with.
|
|
18
|
+
// The /thinking line for a level, with --yes (level_chip.js commandLine);
|
|
19
|
+
// null when it is the session's (+initial+).
|
|
48
20
|
export function commandLine(choice, initial = null) {
|
|
49
21
|
return level.commandLine(CONFIG, choice, initial);
|
|
50
22
|
}
|
|
51
|
-
|
|
52
|
-
// The popover's form (level_chip.js formTemplate).
|
|
53
|
-
export function formTemplate(summary, opts) {
|
|
54
|
-
return level.formTemplate(CONFIG, summary, opts);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// The chip's popover (level_chip.js createLevelControl).
|
|
58
|
-
export function createThinkingControl({ id = "thinkingPopover", ...opts }) {
|
|
59
|
-
return level.createLevelControl({ cfg: CONFIG, id, ...opts });
|
|
60
|
-
}
|
|
@@ -28,7 +28,7 @@ export function timeGapEl(text) {
|
|
|
28
28
|
|
|
29
29
|
// The history items a group (timing.js turnGroups) holds, by index.
|
|
30
30
|
function groupIndexes(group) {
|
|
31
|
-
if (group.kind === "note" || group.kind === "steer") return [group.i];
|
|
31
|
+
if (group.kind === "note" || group.kind === "steer" || group.kind === "command") return [group.i];
|
|
32
32
|
if (group.kind === "answer") return [group.answer];
|
|
33
33
|
const steps = group.steps || [];
|
|
34
34
|
return [
|
|
@@ -54,11 +54,14 @@ export function groupTimes(group, items) {
|
|
|
54
54
|
|
|
55
55
|
// For each group, the divider text before it, or null. A group with no
|
|
56
56
|
// known time draws none and hides the gap across it (no divider measured
|
|
57
|
-
// over a stretch whose times are unknown).
|
|
57
|
+
// over a stretch whose times are unknown). A `!cmd`'s record is saved with
|
|
58
|
+
// no time: it draws none and the gap is measured across it (it ran in that
|
|
59
|
+
// stretch).
|
|
58
60
|
export function groupGaps(groups, items, now = Date.now()) {
|
|
59
61
|
let prevEnd = null;
|
|
60
62
|
return groups.map((group) => {
|
|
61
63
|
const { start, end } = groupTimes(group, items);
|
|
64
|
+
if (group.kind === "command" && start === null) return null;
|
|
62
65
|
const text = prevEnd !== null && start !== null ? gapText(prevEnd, start, now) : null;
|
|
63
66
|
prevEnd = end;
|
|
64
67
|
return text;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isShellCommandItem } from "./shell_command.js";
|
|
2
|
+
|
|
1
3
|
export function formatDuration(milliseconds) {
|
|
2
4
|
const ms = Number(milliseconds);
|
|
3
5
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
@@ -148,8 +150,10 @@ export function turnRecordAt(timing, index) {
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
// A history item that starts a turn: a prompt, or the note a context wake
|
|
151
|
-
// turn starts with (turn_start).
|
|
152
|
-
|
|
153
|
+
// turn starts with (turn_start). A `!cmd`'s record (shell_command.js) is a
|
|
154
|
+
// user message but no prompt: no turn, no record, no timing.
|
|
155
|
+
const startsTurn = (item) =>
|
|
156
|
+
(item?.role === "user" && !isShellCommandItem(item)) || (item?.role === "note" && item.turn_start === true);
|
|
153
157
|
|
|
154
158
|
// For each history item, the turn index its timing shows under, or null. A
|
|
155
159
|
// turn with tool calls holds several assistant messages; only the last one
|
|
@@ -181,7 +185,9 @@ export function timedTurnIndexes(items) {
|
|
|
181
185
|
// assistant message). Assistant messages before any prompt are answers on
|
|
182
186
|
// their own. A turn that ended with no answer (the server's empty_answer
|
|
183
187
|
// item) has no answer: its texts are steps, and `emptyAnswer` is that
|
|
184
|
-
// item's index.
|
|
188
|
+
// item's index. A `!cmd`'s record is a group of its own (`command`, its
|
|
189
|
+
// index): no steps, no record, no turn index. The turn index counts
|
|
190
|
+
// prompts, as timedTurnIndexes does. The
|
|
185
191
|
// user's lines merged into the running turn (`merged`, the server's
|
|
186
192
|
// Steer::INPUT_KIND) stay in it: the turn's `merged` list.
|
|
187
193
|
export function turnGroups(items, timing) {
|
|
@@ -191,6 +197,10 @@ export function turnGroups(items, timing) {
|
|
|
191
197
|
items.forEach((item, i) => {
|
|
192
198
|
if (item?.role === "note" && !startsTurn(item)) {
|
|
193
199
|
groups.push({ kind: "note", i });
|
|
200
|
+
} else if (isShellCommandItem(item)) {
|
|
201
|
+
// A `!cmd`'s record: its bubble alone, between the turns.
|
|
202
|
+
groups.push({ kind: "command", i });
|
|
203
|
+
turn = null;
|
|
194
204
|
} else if (item?.role === "steer") {
|
|
195
205
|
if (turn) turn.steers.push(i);
|
|
196
206
|
else groups.push({ kind: "steer", i });
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
// The session bar's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// session
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
1
|
+
// The session bar's 🧰 chip, after ⚙: the session's toolkit. "🧰" alone
|
|
2
|
+
// while the session is on config's default kit (toolkit.default, full
|
|
3
|
+
// unless set; GET /api/sessions/:id's session.toolkit_default), "🧰 lean"
|
|
4
|
+
// on another; the tooltip names the kit, the tools it leaves out and how to
|
|
5
|
+
// change them. No popover: /toolkit changes it. The summary is the
|
|
6
|
+
// session's toolkit ({name, label, left_out, reachable, removed, fallback}:
|
|
7
|
+
// Toolkit::Explained#summary, null on full; the worker's snapshot or a
|
|
8
|
+
// command_ran; the server's own reading of the session file has no lists).
|
|
9
|
+
// Left out is reachable (find_tools and call_tool reach them) and removed
|
|
10
|
+
// (only /toolkit add brings one back). A kit with a prompt text has it on
|
|
11
|
+
// the summary too (prompt), and the tooltip ends with it.
|
|
10
12
|
|
|
11
13
|
import { html, nothing } from "./vendor/lit-html.js";
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const FULL = "full";
|
|
16
|
+
const CHANGE = "/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.";
|
|
17
|
+
|
|
18
|
+
// The chip's text ("" while on the default kit: 🧰 alone), tooltip and
|
|
19
|
+
// whether it is the default. +summary+: the session's (null: full);
|
|
20
|
+
// +defaultKit+: config's toolkit.default (null: not known, full).
|
|
21
|
+
export function chipModel(summary, defaultKit = null) {
|
|
22
|
+
const fallbackKit = defaultKit || FULL;
|
|
23
|
+
if (!summary) {
|
|
24
|
+
const isDefault = fallbackKit === FULL;
|
|
25
|
+
const title = isDefault ? `Toolkit: ${FULL} (the default), every tool is declared. ${CHANGE}`
|
|
26
|
+
: `Toolkit: ${FULL}, every tool is declared (the default is ${fallbackKit}). ${CHANGE}`;
|
|
27
|
+
return { text: isDefault ? "" : FULL, title, isDefault };
|
|
28
|
+
}
|
|
16
29
|
const label = summary.label || summary.name;
|
|
17
|
-
const
|
|
30
|
+
const isDefault = !summary.fallback && label === fallbackKit;
|
|
31
|
+
const parts = [`Toolkit: ${label}${isDefault ? " (the default)" : ""}, the tools this session declares.`];
|
|
18
32
|
if (summary.fallback) parts.push(`${summary.fallback}: every tool is declared (full) until it resolves.`);
|
|
19
33
|
if (Array.isArray(summary.groups)) {
|
|
20
34
|
parts.push(`Source groups: ${summary.groups.length ? summary.groups.join(", ") : "none"}.`);
|
|
@@ -27,15 +41,17 @@ export function chipModel(summary) {
|
|
|
27
41
|
} else if (Array.isArray(summary.left_out)) {
|
|
28
42
|
parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
|
|
29
43
|
}
|
|
30
|
-
parts.push(
|
|
44
|
+
parts.push(CHANGE);
|
|
31
45
|
if (summary.prompt) parts.push(`Prompt: ${summary.prompt}`);
|
|
32
|
-
return { text:
|
|
46
|
+
return { text: isDefault ? "" : `${label}${summary.fallback ? " ?" : ""}`, title: parts.join(" "), isDefault };
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
// The session bar's chip (a lit-html template; info_bar.js)
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!
|
|
40
|
-
|
|
49
|
+
// The session bar's chip (a lit-html template; info_bar.js): +toolkit+ is
|
|
50
|
+
// {summary, defaultKit}; nothing without it. 🧰 is drawn as the start
|
|
51
|
+
// page's toolkit icon (the glyph stays the text; index.html's CSS).
|
|
52
|
+
export function chipTemplate(toolkit) {
|
|
53
|
+
if (!toolkit) return nothing;
|
|
54
|
+
const chip = chipModel(toolkit.summary, toolkit.defaultKit);
|
|
55
|
+
return html`<span class=${`model tools-chip${chip.isDefault ? " default" : ""}`} title=${chip.title}><span
|
|
56
|
+
class="chip-icon tc-kit" aria-hidden="true">🧰</span>${chip.text ? ` ${chip.text}` : ""}</span>`;
|
|
41
57
|
}
|
|
@@ -840,6 +840,8 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
|
|
|
840
840
|
if (group.kind === "note") { parts.push(noteBubbleTemplate(items[group.i])); continue; }
|
|
841
841
|
if (group.kind === "steer") { parts.push(steerTemplate(group.i, true)); continue; }
|
|
842
842
|
if (group.kind === "answer") { parts.push(answerTemplate(group.answer, nothing)); continue; }
|
|
843
|
+
// A `!cmd`'s record: its command bubble, no steps, no timing.
|
|
844
|
+
if (group.kind === "command") { parts.push(userTemplate(items[group.i])); continue; }
|
|
843
845
|
turnOrdinal += 1;
|
|
844
846
|
const prompt = items[group.user];
|
|
845
847
|
// A context wake turn starts with its note.
|
|
@@ -881,8 +883,11 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
|
|
|
881
883
|
// followed by the text that answered it when that isn't the answer: a
|
|
882
884
|
// small model tells its user about a report before its next tool call,
|
|
883
885
|
// and that step's text would stay folded in the block.
|
|
886
|
+
// A report's fold key (answer_fold.js), as its reply's: the turn's id
|
|
887
|
+
// and its place after the opening; none in a turn saved before turn ids.
|
|
888
|
+
const lineKey = (i) => (group.turnId && items[i]?.source === "delegate_report" ? `${group.turnId}:input:${i - group.user}` : null);
|
|
884
889
|
parts.push((group.merged || []).map((i) =>
|
|
885
|
-
html`${userTemplate(items[i], { state: "steered", step: items[i].step })}${reportReplyTemplate(i, group)}`));
|
|
890
|
+
html`${userTemplate(items[i], { state: "steered", step: items[i].step, lineKey: lineKey(i) })}${reportReplyTemplate(i, group)}`));
|
|
886
891
|
// A turn that ended with no answer: chi's muted notice where the answer
|
|
887
892
|
// would be, before the timing, as live.
|
|
888
893
|
if (group.emptyAnswer != null) parts.push(html`${emptyAnswerTemplate(items[group.emptyAnswer])}${timingTemplate(group)}`);
|
|
@@ -328,7 +328,7 @@ export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStar
|
|
|
328
328
|
case "pending_input_merged":
|
|
329
329
|
if (!unmatchedMerge || !event.content) return [];
|
|
330
330
|
if (unmatchedMerge === "report") {
|
|
331
|
-
return [{ op: "add", enqueuedId: null, prompt: event.content, state: wakeStart ? null : "steered", label: clientLabel(REPORT_CLIENT) }];
|
|
331
|
+
return [{ op: "add", enqueuedId: null, prompt: event.content, state: wakeStart ? null : "steered", label: clientLabel(REPORT_CLIENT), source: "delegate_report" }];
|
|
332
332
|
}
|
|
333
333
|
return [{ op: "add", enqueuedId: null, prompt: event.content, state: "steered", label: null }];
|
|
334
334
|
default:
|
|
@@ -214,18 +214,8 @@ export function stateBadgeTemplate(state, delivery = null) {
|
|
|
214
214
|
return html`<span class="state-badge">${text}${remove}</span>`;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// A `!cmd`'s saved user message
|
|
218
|
-
|
|
219
|
-
// content is any other message. The command sits on the first line inside
|
|
220
|
-
// `!(` `)`; the output is everything after that first newline. A message
|
|
221
|
-
// that merely starts with "!" or "!(" without the newline shape is a plain
|
|
222
|
-
// message.
|
|
223
|
-
export function shellCommandMessage(content) {
|
|
224
|
-
const text = String(content ?? "");
|
|
225
|
-
const match = text.match(/^!\((.*)\)\n([\s\S]*)$/);
|
|
226
|
-
if (!match) return null;
|
|
227
|
-
return { line: `!${match[1]}`, text: match[2] };
|
|
228
|
-
}
|
|
217
|
+
// A `!cmd`'s saved user message as its line and output (shell_command.js).
|
|
218
|
+
export { shellCommandMessage } from "./shell_command.js";
|
|
229
219
|
|
|
230
220
|
// A `!cmd`'s history bubble: the same markup the live page draws for a
|
|
231
221
|
// command (app.js's drawCommandBubble): the line, the output in a
|
|
@@ -316,12 +306,24 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
|
|
|
316
306
|
// colour) in place of +label+. A turn's opening carries its Revise stamp
|
|
317
307
|
// (+message.revises+: data-revises, revise_stack.js) and whether it was a
|
|
318
308
|
// Verify turn (+message.verify+: data-verify, no Revise on its answer).
|
|
319
|
-
|
|
309
|
+
// +message.source+ (a delegate report's "delegate_report") is its
|
|
310
|
+
// data-source; +lineKey+, a merged line's fold key (answer_fold.js:
|
|
311
|
+
// data-line-key, the reload's "<turn id>:input:<place>").
|
|
312
|
+
export function userBubbleTemplate(message, { label = null, state = null, step = null, thumbs = nothing, own = false, enqueuedId = null, key = null, delivery = null, author = null, lineKey = null } = {}) {
|
|
320
313
|
const named = BADGES[state] ? state : null;
|
|
321
314
|
const from = author
|
|
322
315
|
? html`<span class="origin-label author-badge" title=${authorBadgeTitle(author.name, author.mode)} data-unnamed=${author.unnamed ? "1" : nothing}>${authorBadgeText(author.name, author.mode)}</span>`
|
|
323
316
|
: label ? html`<span class="origin-label">${label}</span>` : nothing;
|
|
324
|
-
return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-participant-mode=${author?.mode || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing}
|
|
317
|
+
return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-participant-mode=${author?.mode || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// A user line's text: plain (its quotes set apart), or the server's
|
|
321
|
+
// rendered markdown when the line has it (a delegate report's,
|
|
322
|
+
// message_display.rb; a live line has none until a reload). Its copy
|
|
323
|
+
// source stays the raw text either way (data-copy-source).
|
|
324
|
+
function userMessageTemplate(message, thumbs) {
|
|
325
|
+
if (typeof message?.html === "string") return html`<div class="user-message markdown" .innerHTML=${message.html}></div>`;
|
|
326
|
+
return html`<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>`;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
// The notice of a turn that ended with no answer (the server's
|
|
@@ -69,7 +69,7 @@ module Samagotchi
|
|
|
69
69
|
return error if error
|
|
70
70
|
|
|
71
71
|
# llm_context: the session's own LLM context values before its first
|
|
72
|
-
# turn (the start page's
|
|
72
|
+
# turn (the start page's ⚙ panel; chi --llm-context), read
|
|
73
73
|
# before anything spawns so a bad one spawns nothing.
|
|
74
74
|
llm_context, error = request_llm_context(body["llm_context"])
|
|
75
75
|
return error if error
|
|
@@ -13,6 +13,7 @@ require_relative "../session_commands"
|
|
|
13
13
|
require_relative "../session_metrics"
|
|
14
14
|
require_relative "../session_setting"
|
|
15
15
|
require_relative "../system_one/log"
|
|
16
|
+
require_relative "../toolkit"
|
|
16
17
|
|
|
17
18
|
module Samagotchi
|
|
18
19
|
module Web
|
|
@@ -151,14 +152,16 @@ module Samagotchi
|
|
|
151
152
|
else
|
|
152
153
|
session_json = session_json.merge(saved_served_model(session, persisted))
|
|
153
154
|
end
|
|
154
|
-
# The per-session settings' chips (the info bar's llm ctx, think
|
|
155
|
-
# reasoning
|
|
155
|
+
# The per-session settings' chips (the info bar's ⚙ chip: llm ctx, think
|
|
156
|
+
# and reasoning; the tools chip): the worker's, else worked out here from the
|
|
156
157
|
# session file (SessionSetting#snapshot_value, #saved_summary).
|
|
157
158
|
SessionSetting::ALL.each do |setting|
|
|
158
159
|
chip = setting.snapshot_value(snapshot) { setting.saved_summary(session, registry: host_registry) }
|
|
159
160
|
session_json = session_json.merge(setting.key => chip)
|
|
160
161
|
end
|
|
161
|
-
|
|
162
|
+
# The 🧰 chip reads the session's toolkit against config's default:
|
|
163
|
+
# on it, the chip is 🧰 alone (a toolkit summary is nil on full only).
|
|
164
|
+
session_json.merge(toolkit_default: Toolkit.configured.name)
|
|
162
165
|
end
|
|
163
166
|
|
|
164
167
|
# ?cards=1: the page's re-read when a card arrives, for its rendered
|