samagotchi 0.55.0 → 0.57.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 +65 -1
- data/README.md +1 -1
- data/docs/architecture.md +6 -2
- data/docs/cli.md +30 -8
- data/docs/configuration.md +84 -8
- data/docs/hooks.md +2 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +14 -0
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/memory.md +1 -1
- data/docs/plugins.md +63 -0
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -0
- data/lib/samagotchi/bootstrap/probe.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +2 -0
- data/lib/samagotchi/broadcast_command.rb +2 -0
- data/lib/samagotchi/bundles/core/manifest.yml +2 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +15 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +11 -0
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +320 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +7 -3
- data/lib/samagotchi/context_command.rb +2 -0
- data/lib/samagotchi/desktop/macos.rb +3 -0
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/gem_update.rb +3 -0
- data/lib/samagotchi/guardrails/shell_lex.rb +8 -0
- data/lib/samagotchi/guardrails/shell_paths.rb +3 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +19 -3
- data/lib/samagotchi/installed_versions.rb +3 -0
- 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/plugin/context.rb +5 -0
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/redacted_inspect.rb +28 -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 +23 -5
- 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_runner.rb +14 -3
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/execute.rb +3 -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 +29 -6
- data/lib/samagotchi/web/public/app.js +44 -9
- data/lib/samagotchi/web/public/commands.js +36 -3
- data/lib/samagotchi/web/public/composer.js +5 -1
- data/lib/samagotchi/web/public/index.html +22 -5
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/question_card.js +5 -3
- data/lib/samagotchi/web/public/question_cards.js +21 -1
- data/lib/samagotchi/web/public/read_view.js +74 -0
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/ref_scan.js +206 -0
- data/lib/samagotchi/web/public/refs.js +19 -10
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +10 -2
- data/lib/samagotchi/web/public/thinking_chip.js +33 -138
- data/lib/samagotchi/web/public/turn_html.js +4 -2
- data/lib/samagotchi/web/public/turn_view.js +5 -3
- data/lib/samagotchi/web/token.rb +3 -0
- data/lib/samagotchi/worker.rb +17 -5
- metadata +13 -1
|
@@ -178,6 +178,17 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
178
178
|
.activity-diff>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
179
179
|
.activity-diff>summary:hover{color:var(--text-2)}
|
|
180
180
|
.activity-diff[open]>summary{margin-bottom:4px}
|
|
181
|
+
/* A read of the file as-is: its numbered lines, closed under the preview (read_view.js); open, they stand for it. */
|
|
182
|
+
.activity-read{flex:1 1 100%;min-width:0;margin:2px 0 2px var(--row-gutter)}
|
|
183
|
+
.activity-read>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
184
|
+
.activity-read>summary:hover{color:var(--text-2)}
|
|
185
|
+
.activity-read[open]>summary{margin-bottom:4px}
|
|
186
|
+
.activity-row:has(>.activity-read[open])>.activity-output{display:none}
|
|
187
|
+
.read-lines{margin:0;padding:4px 0;background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;font-size:12px;line-height:1.45;overflow:auto;max-height:420px}
|
|
188
|
+
.read-lines code{display:block;min-width:max-content}
|
|
189
|
+
.read-line{display:block;padding:0 10px 0 0;white-space:pre;color:var(--text-2)}
|
|
190
|
+
.read-line::before{content:attr(data-line);display:inline-block;width:5ch;margin-right:10px;padding-right:6px;text-align:right;color:var(--text-faint);user-select:none;font-variant-numeric:tabular-nums}
|
|
191
|
+
.read-meta{color:var(--text-faint)}
|
|
181
192
|
/* An execute/task_create's full command, above its output and lined up with it: as written, its own scroll. */
|
|
182
193
|
.activity-row>.activity-command{flex:1 1 100%;min-width:0;margin:4px 0 2px var(--row-gutter)}
|
|
183
194
|
.activity-command pre{margin:0;max-height:160px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.45;color:var(--text-2);background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;padding:6px 34px 6px 8px}
|
|
@@ -215,6 +226,7 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
215
226
|
.activity-params{flex:1 1 0}
|
|
216
227
|
.activity-output{margin-left:0}
|
|
217
228
|
.activity-diff{margin-left:0}
|
|
229
|
+
.activity-read{margin-left:0}
|
|
218
230
|
.activity-edit{margin-left:0}
|
|
219
231
|
.activity-row>.activity-command{margin-left:0}
|
|
220
232
|
}
|
|
@@ -307,6 +319,9 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
307
319
|
.bubble.command.queued .command-output{color:var(--text-faint)}
|
|
308
320
|
.command-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-3)}
|
|
309
321
|
.command-output{margin:6px 0 0;white-space:pre-wrap;font-size:12px;color:var(--text-2)}
|
|
322
|
+
.bubble.command .command-confirm{margin-top:8px;padding:3px 10px;border-radius:6px;font:inherit;font-size:12px;cursor:pointer;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
323
|
+
.bubble.command .command-confirm:hover:not(:disabled){border-color:var(--accent)}
|
|
324
|
+
.bubble.command .command-confirm:disabled{opacity:.55;cursor:default}
|
|
310
325
|
.bubble.continue{border-left:3px solid var(--accent);background:var(--bg-panel);padding:10px 12px}
|
|
311
326
|
.bubble.continue.resolved{border-left-color:var(--text-muted);opacity:.8}
|
|
312
327
|
.continue-text{font-size:14px;color:var(--text);margin-bottom:6px}
|
|
@@ -368,8 +383,10 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
368
383
|
/* Refs (refs.js): text until hovered, and only for a kind this viewer has an action for (body.refs-file). */
|
|
369
384
|
.refs-file .ref[data-ref-kind="file"]:hover{color:var(--accent);text-decoration:underline dotted;text-underline-offset:3px;cursor:pointer}
|
|
370
385
|
.refs-file .ref[data-ref-kind="file"]:hover::after{content:"\2197";margin-left:.3em;color:var(--text-faint);display:inline-block}
|
|
371
|
-
.refs-file .activity-row:has(.ref[data-ref-kind="file"]) .diff-line[data-line]:hover{cursor:pointer;box-shadow:inset 2px 0 var(--accent)}
|
|
372
|
-
.refs-file .activity-row:has(.ref[data-ref-kind="file"]) .diff-line[data-line]:hover::after{content:"\2197 " attr(data-line);float:right;padding-left:12px;color:var(--text-faint)}
|
|
386
|
+
.refs-file :is(.activity-row,.bubble.approval):has(.ref[data-ref-kind="file"]) .diff-line[data-line]:hover{cursor:pointer;box-shadow:inset 2px 0 var(--accent)}
|
|
387
|
+
.refs-file :is(.activity-row,.bubble.approval):has(.ref[data-ref-kind="file"]) .diff-line[data-line]:hover::after{content:"\2197 " attr(data-line);float:right;padding-left:12px;color:var(--text-faint)}
|
|
388
|
+
.refs-file .activity-row:has(.ref[data-ref-kind="file"]) .read-line[data-line]:hover{cursor:pointer;box-shadow:inset 2px 0 var(--accent)}
|
|
389
|
+
.refs-file .activity-row:has(.ref[data-ref-kind="file"]) .read-line[data-line]:hover::before{color:var(--accent)}
|
|
373
390
|
.diff-add{background:var(--diff-add-bg);color:var(--diff-add-text)}
|
|
374
391
|
.diff-del{background:var(--diff-del-bg);color:var(--diff-del-text)}
|
|
375
392
|
.diff-hunk{color:var(--text-faint)}
|
|
@@ -704,9 +721,9 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
704
721
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
705
722
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
706
723
|
/* The LLM context and thinking chips (llm_context_chip.js, thinking_chip.js): model-like chips that open their popovers; accent when the session set its own. */
|
|
707
|
-
#infoBar .meta .llmctx-chip,#infoBar .meta .think-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
708
|
-
#infoBar .meta .llmctx-chip:hover,#infoBar .meta .think-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
709
|
-
#infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
724
|
+
#infoBar .meta .llmctx-chip,#infoBar .meta .think-chip,#infoBar .meta .rh-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
725
|
+
#infoBar .meta .llmctx-chip:hover,#infoBar .meta .think-chip:hover,#infoBar .meta .rh-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
726
|
+
#infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own,#infoBar .meta .rh-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
710
727
|
.llmctx-form{display:flex;flex-direction:column;gap:8px;margin:0}
|
|
711
728
|
.llmctx-form .ctx-pop-head strong{font-family:inherit}
|
|
712
729
|
.llmctx-field{display:flex;align-items:center;gap:8px}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The session bar's info line (#infoText): the open session's id, its copy
|
|
2
2
|
// chi --attach button, status, model, chain links, parent and delegates,
|
|
3
|
-
// llm ctx and
|
|
3
|
+
// llm ctx, thinking and reasoning history chips, model notes, memories, folder, ctx, speed,
|
|
4
4
|
// session clock and the worker's version badge. A lit-html template over a
|
|
5
5
|
// plain model app.js builds (updateInfoBar), so the markup lives here and
|
|
6
6
|
// runs under node --test (spec/web/public/info_bar.test.js).
|
|
@@ -15,6 +15,7 @@ import { memChipText, memChipTitle } from "./format.js";
|
|
|
15
15
|
import { ALL_SESSIONS_HASH, sessionHash } from "./route.js";
|
|
16
16
|
import { chipTemplate as llmContextChip } from "./llm_context_chip.js";
|
|
17
17
|
import { chipTemplate as thinkingChip } from "./thinking_chip.js";
|
|
18
|
+
import { chipTemplate as reasoningHistoryChip } from "./reasoning_history_chip.js";
|
|
18
19
|
|
|
19
20
|
export const PREV = "←";
|
|
20
21
|
export const NEXT = "→";
|
|
@@ -60,7 +61,8 @@ function kidsTemplate(kids) {
|
|
|
60
61
|
// id, copied (the copy button just copied), status, model ({text, title,
|
|
61
62
|
// mismatch}: format.js modelLabel), chain ({prev, next}: chainLinkTemplate's
|
|
62
63
|
// links or null), parent ({id, title} or null), kids (format.js
|
|
63
|
-
// childrenSummary), llmContext and
|
|
64
|
+
// childrenSummary), llmContext, thinking and reasoningHistory (the chips'
|
|
65
|
+
// summaries; reasoningHistory null on a native host: no chip), notes
|
|
64
66
|
// ({text, title} or null), memories (the used names), dir, ctx ({text,
|
|
65
67
|
// tip}), speed ({text, tip} or null), clock (the session time's text, ""
|
|
66
68
|
// for none), worker ({badge, busy} or null).
|
|
@@ -68,7 +70,7 @@ export function infoBarHtml(bar) {
|
|
|
68
70
|
if (!bar) return nothing;
|
|
69
71
|
const attach = `chi --attach ${bar.id}`;
|
|
70
72
|
const memories = bar.memories || [];
|
|
71
|
-
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
73
|
+
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip.
|
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,7 @@ export const RECENT_KEY = "chi_model_recent";
|
|
|
22
22
|
|
|
23
23
|
// The payload's models as rows: {name, host, id, isDefaultHost}, and
|
|
24
24
|
// sampling (the configured request parameters as /model words them) when
|
|
25
|
-
// the model has some, thinking (the level it starts at,
|
|
25
|
+
// the model has some, thinking (the level it starts at, SettingExplained#summary),
|
|
26
26
|
// and llm_context (the LLM context it starts under,
|
|
27
27
|
// LLMContextStrategy::Explained#summary) when the server sent one, and
|
|
28
28
|
// configured when the host declares the id (hosts.<name>.models), and
|
|
@@ -49,9 +49,10 @@ export function relayView(pq) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// @return {null | {tool, what, isCommand, where, why, preview?, delegate?}};
|
|
53
|
-
//
|
|
54
|
-
// is
|
|
52
|
+
// @return {null | {tool, what, isCommand, where, why, paths?, preview?, delegate?}};
|
|
53
|
+
// paths are what +what+ joins when it shows paths (the card draws a ref
|
|
54
|
+
// for each); preview is an edit/write's dry-run diff (diff_view.js draws
|
|
55
|
+
// it); delegate is relayView's, for a delegate's approval
|
|
55
56
|
export function approvalView(pq) {
|
|
56
57
|
if (!isApproval(pq)) return null;
|
|
57
58
|
const a = pq.approval || {};
|
|
@@ -74,6 +75,7 @@ export function approvalView(pq) {
|
|
|
74
75
|
// A tool without a command or path (an MCP tool): its compact args.
|
|
75
76
|
what: command || paths.join("\n") || a.args || "",
|
|
76
77
|
isCommand: !!command,
|
|
78
|
+
...(!command && paths.length ? { paths } : {}),
|
|
77
79
|
where,
|
|
78
80
|
why: `${reason} (${who})`,
|
|
79
81
|
...(a.preview && typeof a.preview === "object" ? { preview: a.preview } : {}),
|
|
@@ -254,7 +254,8 @@ export function createQuestionCards({
|
|
|
254
254
|
const what = doc.createElement("pre");
|
|
255
255
|
what.className = view.isCommand ? "approval-what command" : "approval-what paths";
|
|
256
256
|
const code = doc.createElement("code");
|
|
257
|
-
|
|
257
|
+
if (view.paths) appendPathRefs(code, view.paths);
|
|
258
|
+
else code.textContent = view.what;
|
|
258
259
|
what.appendChild(code);
|
|
259
260
|
box.appendChild(what);
|
|
260
261
|
}
|
|
@@ -279,6 +280,25 @@ export function createQuestionCards({
|
|
|
279
280
|
return box;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
// One file ref per path, a line each (refs.js: the card's path opens
|
|
284
|
+
// the current file at the dry run's first change). A directory (a
|
|
285
|
+
// plugin's path ending in /) stays plain text.
|
|
286
|
+
function appendPathRefs(code, paths) {
|
|
287
|
+
paths.forEach((path, i) => {
|
|
288
|
+
if (i) code.appendChild(doc.createTextNode("\n"));
|
|
289
|
+
if (String(path).endsWith("/")) {
|
|
290
|
+
code.appendChild(doc.createTextNode(String(path)));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const ref = doc.createElement("span");
|
|
294
|
+
ref.className = "ref";
|
|
295
|
+
ref.dataset.refKind = "file";
|
|
296
|
+
ref.dataset.refPath = String(path);
|
|
297
|
+
ref.textContent = String(path);
|
|
298
|
+
code.appendChild(ref);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
282
302
|
// "Waiting for approval in parent ab12: answering here works too", or
|
|
283
303
|
// an empty (hidden) line while the question isn't relayed.
|
|
284
304
|
function renderRelayedTo(pq) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// A read row's output as the file's lines, numbered, apart from the DOM.
|
|
2
|
+
// Only when the output is the file text as-is: each numbered line is a
|
|
3
|
+
// ref's line (refs.js opens the row's file there). The row's preview
|
|
4
|
+
// (turn_html.js) strips any "[tag] " and cuts at 300 chars; numbering
|
|
5
|
+
// needs the text exactly as read, so it is drawn on its own, closed.
|
|
6
|
+
|
|
7
|
+
import { escapeHtml } from "./format.js";
|
|
8
|
+
|
|
9
|
+
// What follows one of these isn't the file's next line: a ToolRunner cap
|
|
10
|
+
// ("[cut: …]"), a head/tail preview's gap ("[... omitted …]") or the note
|
|
11
|
+
// of a range clamped at the end of the file ("[read: end_line …]"). They
|
|
12
|
+
// are appended at the end (a clamp note, then a cut), so only the last
|
|
13
|
+
// MARKER_LINES lines can be one: such a line earlier is the file's own.
|
|
14
|
+
const MARKER_LINES = 2;
|
|
15
|
+
const MARKER = /^(?:\[cut: |\[\.\.\. omitted |\[read: end_line \d+ exceeds )/;
|
|
16
|
+
|
|
17
|
+
// An image's line instead of its text: attached, or why not (ToolRunner).
|
|
18
|
+
const IMAGE = /\((?:\d+×\d+ )?[A-Z]+\)(?: attached\.| is an image; | is not attached: )/;
|
|
19
|
+
|
|
20
|
+
// The file text of a read row's output, or null when it isn't the file
|
|
21
|
+
// as-is: an error, the head/tail preview of a big file, an image's line.
|
|
22
|
+
function fileText(row) {
|
|
23
|
+
if (row?.tool !== "read" || row.status !== "ok" || row.ref?.kind !== "file" || row.images?.length) return null;
|
|
24
|
+
let text = String(row.output || "");
|
|
25
|
+
// The call the gate replaced says so on a line before the tag.
|
|
26
|
+
if (text.startsWith("ran as: ")) text = text.slice(text.indexOf("\n") + 1);
|
|
27
|
+
if (!text.startsWith("[read]\n")) return null;
|
|
28
|
+
text = text.slice("[read]\n".length);
|
|
29
|
+
if (!text || text.startsWith("Error: ") || text.startsWith("truncated=true\n")) return null;
|
|
30
|
+
if (!text.includes("\n") && IMAGE.test(text)) return null;
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// @return {null | Array<{text, line}>} the output's lines, line null for
|
|
35
|
+
// one that isn't the file's: a marker and all after it, and the "…" of a
|
|
36
|
+
// reload's capped output (output_truncated; a cut line already says so
|
|
37
|
+
// for a live one). The first line is the range's start_line (the ref's
|
|
38
|
+
// line), else 1.
|
|
39
|
+
export function readLines(row) {
|
|
40
|
+
const text = fileText(row);
|
|
41
|
+
if (text === null) return null;
|
|
42
|
+
const lines = text.split("\n");
|
|
43
|
+
// The file's last newline ends a line, it doesn't start one.
|
|
44
|
+
if (lines.at(-1) === "") lines.pop();
|
|
45
|
+
let next = Number(row.ref.line) || 1;
|
|
46
|
+
let numbering = true;
|
|
47
|
+
const out = [];
|
|
48
|
+
lines.forEach((line, i) => {
|
|
49
|
+
if (numbering && i >= lines.length - MARKER_LINES && MARKER.test(line)) {
|
|
50
|
+
numbering = false;
|
|
51
|
+
// The newline before a marker is the marker's: the blank it leaves
|
|
52
|
+
// isn't a line of the file.
|
|
53
|
+
if (i > 0 && lines[i - 1] === "" && out.at(-1)?.line !== null) out.pop();
|
|
54
|
+
}
|
|
55
|
+
out.push({ text: line, line: numbering ? next++ : null });
|
|
56
|
+
});
|
|
57
|
+
if (row.output_truncated && numbering) out.push({ text: "…", line: null });
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A read row's numbered lines behind a closed toggle ("lines 40–60"); ""
|
|
62
|
+
// when its output isn't the file as-is.
|
|
63
|
+
export function readLinesHtml(row) {
|
|
64
|
+
const lines = readLines(row);
|
|
65
|
+
const numbered = lines?.filter((l) => l.line !== null) || [];
|
|
66
|
+
if (!numbered.length) return "";
|
|
67
|
+
const first = numbered[0].line;
|
|
68
|
+
const last = numbered.at(-1).line;
|
|
69
|
+
const label = first === last ? `line ${first}` : `lines ${first}–${last}`;
|
|
70
|
+
const spans = lines.map(({ text, line }) => line === null
|
|
71
|
+
? `<span class="read-line read-meta">${escapeHtml(text)}</span>`
|
|
72
|
+
: `<span class="read-line" data-line="${line}">${escapeHtml(text) || " "}</span>`).join("");
|
|
73
|
+
return `<details class="activity-read"><summary>${label}</summary><pre class="read-lines"><code>${spans}</code></pre></details>`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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.
|
|
8
|
+
|
|
9
|
+
import * as level from "./level_chip.js";
|
|
10
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
11
|
+
|
|
12
|
+
export const LEVELS = ["drop", "turn", "all"];
|
|
13
|
+
|
|
14
|
+
export const CONFIG = Object.freeze({
|
|
15
|
+
name: "Reasoning history",
|
|
16
|
+
prefix: "reasoning",
|
|
17
|
+
command: "/reasoning-history",
|
|
18
|
+
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.",
|
|
24
|
+
});
|
|
25
|
+
|
|
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
|
+
|
|
38
|
+
export function commandLine(choice, initial = null) {
|
|
39
|
+
return level.commandLine(CONFIG, choice, initial);
|
|
40
|
+
}
|
|
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
|
+
}
|