samagotchi 0.54.0 → 0.56.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 +50 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +6 -2
- data/docs/cli.md +29 -7
- data/docs/configuration.md +113 -8
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/context-telemetry.md +23 -5
- data/docs/internals/engine.md +54 -18
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/sessions.md +2 -2
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -6
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +9 -3
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/kernel_loop.rb +99 -16
- data/lib/samagotchi/llm/chat_loop.rb +122 -71
- data/lib/samagotchi/llm/openai_chat.rb +2 -1
- data/lib/samagotchi/llm/stream_drops.rb +63 -0
- data/lib/samagotchi/llm/turn_settings.rb +9 -5
- data/lib/samagotchi/model_settings.rb +5 -2
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/self_report.rb +21 -0
- data/lib/samagotchi/session.rb +23 -2
- data/lib/samagotchi/session_commands.rb +55 -15
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_setup.rb +7 -5
- data/lib/samagotchi/setting_explained.rb +20 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +13 -6
- data/lib/samagotchi/thinking.rb +20 -25
- data/lib/samagotchi/thinking_command.rb +33 -11
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/peers.rb +4 -2
- data/lib/samagotchi/tools/task_runtime.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -4
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +33 -18
- data/lib/samagotchi/web/public/app.js +256 -1133
- data/lib/samagotchi/web/public/commands.js +269 -0
- data/lib/samagotchi/web/public/composer.js +105 -0
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +11 -3
- data/lib/samagotchi/web/public/info_bar.js +80 -0
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +2 -1
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +57 -0
- data/lib/samagotchi/web/public/stream_handlers.js +332 -0
- data/lib/samagotchi/web/public/thinking_chip.js +38 -144
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +5 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/worker.rb +17 -5
- metadata +18 -2
- data/lib/samagotchi/web/public/stable_html.js +0 -18
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// A level chip in the session bar and its popover: the level the next turn
|
|
2
|
+
// runs at ("think low", "reasoning all"; "· session" when the session set
|
|
3
|
+
// its own) and a form with a level select that runs the setting's command
|
|
4
|
+
// in the session's worker (the reply shows in the conversation, like a
|
|
5
|
+
// typed one). The summary is the setting's SettingExplained#summary
|
|
6
|
+
// ({level, source, own}: the worker's snapshot, a command_ran, or the
|
|
7
|
+
// server's own reading of the session file).
|
|
8
|
+
//
|
|
9
|
+
// A config names the setting (thinking_chip.js, reasoning_history_chip.js):
|
|
10
|
+
// name the popover's heading ("Thinking")
|
|
11
|
+
// prefix the chip's word ("think")
|
|
12
|
+
// command the command ("/thinking")
|
|
13
|
+
// levels the settable levels (the select's options after "follow")
|
|
14
|
+
// anchor the chip's selector (its data attribute: each config's
|
|
15
|
+
// chipTemplate draws it; clicks are delegated on the same
|
|
16
|
+
// element as the other chips')
|
|
17
|
+
// formClass, popoverClass, nowClass the form's, popover's and "Now:"
|
|
18
|
+
// line's own classes
|
|
19
|
+
// when(fresh) the form's note on when it applies (fresh: the start
|
|
20
|
+
// page's new chat, thinking only)
|
|
21
|
+
//
|
|
22
|
+
// The start page's new chat (kind NEW_CHAT) has a thinking chip only
|
|
23
|
+
// (thinking_chip.js keeps its choice): the form shows the model's level and
|
|
24
|
+
// takes a choice for the next create.
|
|
25
|
+
|
|
26
|
+
import { NEW_CHAT, SESSION, popoverPlace } from "./llm_context_chip.js";
|
|
27
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
28
|
+
|
|
29
|
+
export const DEFAULT = "default";
|
|
30
|
+
|
|
31
|
+
// The chip: its text, tooltip and whether the session (or the new chat)
|
|
32
|
+
// set its own; null without a summary (an older worker, or a host the
|
|
33
|
+
// setting means nothing on).
|
|
34
|
+
export function chipModel(cfg, summary, { kind = SESSION } = {}) {
|
|
35
|
+
if (!summary || !summary.level) return null;
|
|
36
|
+
const own = !!summary.own;
|
|
37
|
+
const what = kind === NEW_CHAT ? "the new chat" : "this session";
|
|
38
|
+
const from = own ? what : summary.source || "the model's own";
|
|
39
|
+
const title = `${cfg.name}: ${summary.level} (${from}). Click to change it for ${what}.`;
|
|
40
|
+
return { text: `${cfg.prefix} ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The command line for a select value ("default" unsets the session's own),
|
|
44
|
+
// with --yes: picking a level in the form is the choice, and the form
|
|
45
|
+
// names the cost (CacheConfirm asks nothing then); null when it is what
|
|
46
|
+
// the form opened with.
|
|
47
|
+
export function commandLine(cfg, level, initial = null) {
|
|
48
|
+
if (initial !== null && level === initial) return null;
|
|
49
|
+
return `${cfg.command} ${level} --yes`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// "Now: low (session)", "Now: default (the model's own)".
|
|
53
|
+
export function nowText(summary) {
|
|
54
|
+
return `Now: ${summary.level} (${summary.source || "the model's own"})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
|
|
58
|
+
|
|
59
|
+
// The popover's form (a lit-html template). For the new chat (kind
|
|
60
|
+
// NEW_CHAT), +summary+ is the model's and +choice+ the level the form
|
|
61
|
+
// shows; +now+ is the summary its "Now:" line reads (refresh passes a
|
|
62
|
+
// newer one, the rest stays as the form opened).
|
|
63
|
+
export function formTemplate(cfg, summary, { running = false, kind = SESSION, choice = null, now = summary } = {}) {
|
|
64
|
+
const fresh = kind === NEW_CHAT;
|
|
65
|
+
const chosen = (fresh ? choice : summary.own) || DEFAULT;
|
|
66
|
+
const follow = summary.own && !fresh ? "follow the model" : `follow the model (${summary.level})`;
|
|
67
|
+
return html`<form class=${`${cfg.formClass} llmctx-form`}><div class="ctx-pop-head"><strong>${cfg.name}</strong
|
|
68
|
+
><span class="ctx-pop-kind">${fresh ? NEW_CHAT : "this session"}</span
|
|
69
|
+
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
70
|
+
><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
|
|
71
|
+
><label class="llmctx-field"><span>Level</span><select name="level">${option(DEFAULT, follow, chosen)}${
|
|
72
|
+
cfg.levels.map((level) => option(level, level, chosen))}</select></label
|
|
73
|
+
><div class="ctx-pop-meta">${cfg.when(fresh)}${running ? " A turn is running: it applies after this turn." : ""}</div
|
|
74
|
+
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The chip's popover, as createLlmContextControl's: el is where the chip
|
|
78
|
+
// is drawn (clicks are delegated: #infoText, info_bar.js);
|
|
79
|
+
// summary(): the open session's summary; session(): its id; running(): a
|
|
80
|
+
// turn runs; run(line): sends the command. The start page's (kind
|
|
81
|
+
// NEW_CHAT): summary() is the picked model's, choice() the pending level
|
|
82
|
+
// and choose(next) takes a Set's (null for "follow the model"). id and
|
|
83
|
+
// anchor (the chip's selector, by default its data attribute) keep it
|
|
84
|
+
// apart from the other controls on the same element (the anchor defaults
|
|
85
|
+
// to the config's).
|
|
86
|
+
export function createLevelControl({ cfg, el, summary, session = () => null, running = () => false, run,
|
|
87
|
+
kind = SESSION, choice = () => null, choose = null,
|
|
88
|
+
id, anchor: anchorSel = cfg.anchor,
|
|
89
|
+
doc = document, win = window }) {
|
|
90
|
+
const fresh = kind === NEW_CHAT;
|
|
91
|
+
const pop = doc.createElement("div");
|
|
92
|
+
pop.id = id;
|
|
93
|
+
pop.className = `ctx-popover ${cfg.popoverClass}`;
|
|
94
|
+
pop.setAttribute("role", "dialog");
|
|
95
|
+
pop.hidden = true;
|
|
96
|
+
doc.body.appendChild(pop);
|
|
97
|
+
let initial = null;
|
|
98
|
+
let openFor = null;
|
|
99
|
+
// What the open form was drawn from: the model's or session's summary
|
|
100
|
+
// and the form's options. A closed popover is empty, so each open draws
|
|
101
|
+
// a fresh form (an unsent pick is gone).
|
|
102
|
+
let shown = null;
|
|
103
|
+
const draw = (now) => render(formTemplate(cfg, shown.data, { ...shown.opts, now }), pop);
|
|
104
|
+
|
|
105
|
+
function place(anchor) {
|
|
106
|
+
const width = Math.min(320, win.innerWidth - 32);
|
|
107
|
+
pop.style.width = `${width}px`;
|
|
108
|
+
pop.style.maxHeight = "";
|
|
109
|
+
const at = popoverPlace(anchor.getBoundingClientRect(), { width, height: pop.offsetHeight,
|
|
110
|
+
innerWidth: win.innerWidth, innerHeight: win.innerHeight });
|
|
111
|
+
pop.style.left = `${at.left}px`;
|
|
112
|
+
pop.style.top = at.top === undefined ? "" : `${at.top}px`;
|
|
113
|
+
pop.style.bottom = at.bottom === undefined ? "" : `${at.bottom}px`;
|
|
114
|
+
if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function open(anchor) {
|
|
118
|
+
const data = summary();
|
|
119
|
+
if (!data) return;
|
|
120
|
+
initial = (fresh ? choice() : data.own) || DEFAULT;
|
|
121
|
+
openFor = session();
|
|
122
|
+
shown = { data, opts: { running: running(), kind, choice: choice() } };
|
|
123
|
+
draw(data);
|
|
124
|
+
pop.hidden = false;
|
|
125
|
+
place(anchor);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function close() {
|
|
129
|
+
pop.hidden = true;
|
|
130
|
+
initial = null;
|
|
131
|
+
openFor = null;
|
|
132
|
+
shown = null;
|
|
133
|
+
render(nothing, pop);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isOpen() { return !pop.hidden; }
|
|
137
|
+
|
|
138
|
+
// A new summary: an open form keeps the user's pick; only "Now:" follows
|
|
139
|
+
// (the same template again: lit writes that line alone).
|
|
140
|
+
function refresh() {
|
|
141
|
+
if (!isOpen()) return;
|
|
142
|
+
const data = summary();
|
|
143
|
+
if (!data || session() !== openFor) return close();
|
|
144
|
+
draw(data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
el.addEventListener("click", (e) => {
|
|
148
|
+
const anchor = e.target.closest(anchorSel);
|
|
149
|
+
if (!anchor) return;
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
if (isOpen()) close();
|
|
152
|
+
else open(anchor);
|
|
153
|
+
});
|
|
154
|
+
pop.addEventListener("submit", (e) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
const level = e.target.elements.level.value;
|
|
157
|
+
const was = initial;
|
|
158
|
+
close();
|
|
159
|
+
if (fresh) return choose(level === DEFAULT ? null : level);
|
|
160
|
+
const line = commandLine(cfg, level, was);
|
|
161
|
+
if (line) run(line);
|
|
162
|
+
});
|
|
163
|
+
pop.addEventListener("click", (e) => {
|
|
164
|
+
if (e.target.closest("[data-act]")?.dataset.act === "close") close();
|
|
165
|
+
});
|
|
166
|
+
doc.addEventListener("click", (e) => {
|
|
167
|
+
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(anchorSel)) close();
|
|
168
|
+
});
|
|
169
|
+
doc.addEventListener("keydown", (e) => {
|
|
170
|
+
if (e.key === "Escape" && isOpen()) close();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return { close, refresh, get open() { return isOpen(); } };
|
|
174
|
+
}
|