samagotchi 0.24.0 → 0.26.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 +80 -1
- data/docs/architecture.md +16 -9
- data/docs/cli.md +4 -3
- data/docs/configuration.md +22 -13
- data/docs/hooks.md +1 -1
- data/docs/internals/background-tasks.md +1 -0
- data/docs/internals/engine.md +478 -0
- data/docs/internals/prompt-caching.md +120 -0
- data/docs/plugins.md +18 -3
- data/docs/testing.md +12 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +59 -11
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cancellation_controller.rb +8 -0
- data/lib/samagotchi/client.rb +53 -3
- data/lib/samagotchi/command_steps.rb +233 -0
- data/lib/samagotchi/config.rb +14 -4
- data/lib/samagotchi/empty_answer_retry.rb +2 -2
- data/lib/samagotchi/engine.rb +73 -11
- data/lib/samagotchi/guardrails/shell_lex.rb +49 -5
- data/lib/samagotchi/idle_client.rb +14 -2
- data/lib/samagotchi/kernel_loop.rb +60 -6
- data/lib/samagotchi/llm/chat_loop.rb +18 -4
- data/lib/samagotchi/llm/errors.rb +26 -1
- data/lib/samagotchi/llm/http.rb +5 -4
- data/lib/samagotchi/llm/openai_chat.rb +5 -4
- data/lib/samagotchi/llm/prompt_cache.rb +34 -9
- data/lib/samagotchi/llm/usage.rb +18 -3
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/prompt_warmup.rb +95 -0
- data/lib/samagotchi/session_metrics.rb +38 -4
- data/lib/samagotchi/system_prompt.rb +30 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_view.rb +6 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +14 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +31 -9
- data/lib/samagotchi/terminal_ui.rb +2 -0
- data/lib/samagotchi/thinking_tails.rb +104 -0
- data/lib/samagotchi/token_usage.rb +18 -6
- data/lib/samagotchi/tool_activity.rb +37 -3
- data/lib/samagotchi/tool_declarations.rb +14 -0
- data/lib/samagotchi/tool_view.rb +40 -4
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/execute.rb +2 -1
- data/lib/samagotchi/tools/registry.rb +11 -4
- data/lib/samagotchi/tools/shell.rb +32 -0
- data/lib/samagotchi/tools/task_runtime.rb +3 -3
- data/lib/samagotchi/turn_note.rb +26 -2
- data/lib/samagotchi/turn_state.rb +6 -0
- data/lib/samagotchi/turn_tally.rb +17 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/command_view.js +74 -3
- data/lib/samagotchi/web/public/format.js +11 -1
- data/lib/samagotchi/web/public/index.html +45 -13
- data/lib/samagotchi/web/public/stage_model.js +5 -1
- data/lib/samagotchi/web/public/stage_view.js +1 -1
- data/lib/samagotchi/web/public/turn_view.js +3 -3
- data/lib/samagotchi/worker.rb +3 -0
- metadata +7 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "tool_view"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
# A running count of one turn's tool calls, for the one-line tally a long,
|
|
5
7
|
# tool-heavy turn shows under its activity row:
|
|
@@ -32,10 +34,13 @@ module Samagotchi
|
|
|
32
34
|
|
|
33
35
|
# @param key [Object] one call's identity within the turn (the event's
|
|
34
36
|
# [iteration, call_index])
|
|
35
|
-
|
|
37
|
+
# @param description [String, nil] a command's description, which the
|
|
38
|
+
# last call shows in place of its params
|
|
39
|
+
def started(key:, tool:, params: nil, description: nil)
|
|
36
40
|
call = (@calls[key] ||= { tool: tool.to_s, params: "", status: "running" })
|
|
37
41
|
call[:tool] = tool.to_s unless tool.to_s.empty?
|
|
38
42
|
call[:params] = params.to_s unless params.nil?
|
|
43
|
+
call[:description] = description if description
|
|
39
44
|
call[:status] = "running"
|
|
40
45
|
call
|
|
41
46
|
end
|
|
@@ -43,9 +48,10 @@ module Samagotchi
|
|
|
43
48
|
# A completion with no start seen still counts as a call (as the web's
|
|
44
49
|
# activity rows do). Only "error" is a failure: a call a guardrail or an
|
|
45
50
|
# approval blocked still counts as a call, not as a failed one.
|
|
46
|
-
def completed(key:, tool:, status:, params: nil)
|
|
47
|
-
call = @calls[key] || started(key: key, tool: tool, params: params)
|
|
51
|
+
def completed(key:, tool:, status:, params: nil, description: nil)
|
|
52
|
+
call = @calls[key] || started(key: key, tool: tool, params: params, description: description)
|
|
48
53
|
call[:params] = params.to_s if params
|
|
54
|
+
call[:description] = description if description
|
|
49
55
|
call[:status] = status.to_s == "error" ? "error" : "ok"
|
|
50
56
|
call
|
|
51
57
|
end
|
|
@@ -58,7 +64,8 @@ module Samagotchi
|
|
|
58
64
|
next unless part[:kind].to_s == "tool"
|
|
59
65
|
|
|
60
66
|
key = [part[:iteration].to_i, part[:call_index].to_i]
|
|
61
|
-
|
|
67
|
+
view = part[:view].is_a?(Hash) ? part[:view].transform_keys(&:to_sym) : {}
|
|
68
|
+
started(key: key, tool: part[:tool], params: part[:params], description: ToolView.description_title(view))
|
|
62
69
|
completed(key: key, tool: part[:tool], status: part[:status]) unless part[:status].to_s == "running"
|
|
63
70
|
end
|
|
64
71
|
self
|
|
@@ -85,8 +92,12 @@ module Samagotchi
|
|
|
85
92
|
fields = [head] + top.map { |(tool, n), _| "#{tool} ×#{n}" }
|
|
86
93
|
if last
|
|
87
94
|
call = calls.last
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
shown = if call[:description]
|
|
96
|
+
"#{call[:tool]}: #{call[:description]}"
|
|
97
|
+
else
|
|
98
|
+
[call[:tool], call[:params].to_s.gsub(/\s+/, " ").strip].reject(&:empty?).join(" ")
|
|
99
|
+
end
|
|
100
|
+
fields << "last: #{shown}"
|
|
90
101
|
end
|
|
91
102
|
cut(fields.join(SEPARATOR), width)
|
|
92
103
|
end
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -17,12 +17,83 @@ export function rowHover(row) {
|
|
|
17
17
|
// given: execute's may be relative to the project root), the command as
|
|
18
18
|
// written (whitespace kept, its own scroll), a copy button, and a line
|
|
19
19
|
// saying how much a cut command left out. "" for a call without a view.
|
|
20
|
-
|
|
20
|
+
//
|
|
21
|
+
// A view with steps (CommandSteps) shows them in the command's place, its
|
|
22
|
+
// leading cd as one more "in" tag, and a "raw" toggle (a checkbox, CSS
|
|
23
|
+
// only, so a live and a reloaded row behave alike; beside the first step
|
|
24
|
+
// when there's no "in" tag) that swaps the steps for the command as
|
|
25
|
+
// written. The copy button copies the raw command
|
|
26
|
+
// either way. A view without steps (the parser's fallback) is the raw
|
|
27
|
+
// block alone: nothing hides the command.
|
|
28
|
+
//
|
|
29
|
+
// +title+ is the row's: the model's description of the command, when it
|
|
30
|
+
// gave one, cut to fit. The block starts with the whole description when
|
|
31
|
+
// the title is cut, never in place of the command.
|
|
32
|
+
export function commandBlockHtml(view, title = null) {
|
|
21
33
|
const command = view?.command;
|
|
22
34
|
if (!command) return "";
|
|
23
|
-
const
|
|
35
|
+
const desc = view.description && view.description !== title
|
|
36
|
+
? `<div class="activity-command-desc">${escapeHtml(view.description)}</div>`
|
|
37
|
+
: "";
|
|
38
|
+
const steps = Array.isArray(view.steps) && view.steps.length ? view.steps : null;
|
|
39
|
+
const dirs = [view.cwd, steps && view.cd].filter(Boolean);
|
|
40
|
+
const where = dirs.length
|
|
41
|
+
? `in ${dirs.map((d) => `<span>${escapeHtml(d)}</span>`).join(" <span class=\"activity-command-sep\">›</span> ")}`
|
|
42
|
+
: "";
|
|
24
43
|
const cut = view.truncated
|
|
25
44
|
? `<div class="activity-command-cut">… (${command.length.toLocaleString("en-US")} of ${Number(view.chars || 0).toLocaleString("en-US")} chars)</div>`
|
|
26
45
|
: "";
|
|
27
|
-
|
|
46
|
+
const raw = `<pre><code>${escapeHtml(command)}</code></pre>`;
|
|
47
|
+
if (!steps) {
|
|
48
|
+
const cwd = where ? `<div class="activity-command-cwd">${where}</div>` : "";
|
|
49
|
+
return `<div class="activity-command code-wrap">${desc}${cwd}${raw}${copyButtonHtml("code")}${cut}</div>`;
|
|
50
|
+
}
|
|
51
|
+
// No "in" tag: the toggle alone, floated beside the first step (an
|
|
52
|
+
// empty line for it would sit above the steps).
|
|
53
|
+
const head = (where ? `<div class="activity-command-head"><span class="activity-command-cwd">${where}</span>` : `<div class="activity-command-head bare">`) +
|
|
54
|
+
`<label class="activity-command-raw" title="Show the command as written"><input type="checkbox">raw</label></div>`;
|
|
55
|
+
return `<div class="activity-command code-wrap has-steps">${desc}${head}${stepsHtml(steps)}${raw}${copyButtonHtml("code")}${cut}</div>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// What an op between two steps reads as: an arrow for "then" (&&, ;, a
|
|
59
|
+
// new line, a pipe), "else" for ||, "&" for one put in the background.
|
|
60
|
+
const OP_GLYPHS = { "&&": "→", ";": "→", "\n": "→", "|": "→", "|&": "→", "||": "else", "&": "&" };
|
|
61
|
+
const OP_NAMES = { "\n": "new line", "&": "after starting the step before in the background" };
|
|
62
|
+
|
|
63
|
+
function opHtml(op) {
|
|
64
|
+
if (!op) return `<span class="step-op"></span>`;
|
|
65
|
+
const glyph = OP_GLYPHS[op] || op;
|
|
66
|
+
const name = OP_NAMES[op] || op;
|
|
67
|
+
return `<span class="step-op${op === "||" ? " else" : ""}" title="${escapeHtml(name)}">${escapeHtml(glyph)}</span>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A step's redirections to or from nowhere (2>&1, 2>/dev/null, </dev/null),
|
|
71
|
+
// dimmed: plumbing, not what the step does.
|
|
72
|
+
const PLUMBING = /(^|\s)(\d*>&\d+|&>\/dev\/null|\d*>>?\s*\/dev\/null|<\s*\/dev\/null)(?=\s|$)/g;
|
|
73
|
+
|
|
74
|
+
export function stepTextHtml(text) {
|
|
75
|
+
let out = "";
|
|
76
|
+
let at = 0;
|
|
77
|
+
for (const m of String(text).matchAll(PLUMBING)) {
|
|
78
|
+
const start = m.index + m[1].length;
|
|
79
|
+
out += `${escapeHtml(text.slice(at, start))}<span class="step-plumbing">${escapeHtml(m[2])}</span>`;
|
|
80
|
+
at = start + m[2].length;
|
|
81
|
+
}
|
|
82
|
+
return out + escapeHtml(String(text).slice(at));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The steps as a list: a label as a small heading above its step, the op
|
|
86
|
+
// that links a step to the one before, its text, its limit and heredoc
|
|
87
|
+
// as chips (beside the text, below it when the row is narrow).
|
|
88
|
+
export function stepsHtml(steps) {
|
|
89
|
+
const items = steps.map((step) => {
|
|
90
|
+
const label = step.label ? `<li class="step-label">${escapeHtml(step.label)}</li>` : "";
|
|
91
|
+
const limit = step.limit ? `<span class="step-chip" title="output limited">${escapeHtml(step.limit)}</span>` : "";
|
|
92
|
+
const heredoc = step.heredoc
|
|
93
|
+
? `<span class="step-chip heredoc" title="a heredoc: its text in the raw command">${escapeHtml(step.heredoc.tag)} · ` +
|
|
94
|
+
`${Number(step.heredoc.lines)} ${step.heredoc.lines === 1 ? "line" : "lines"}</span>`
|
|
95
|
+
: "";
|
|
96
|
+
return `${label}<li class="activity-step">${opHtml(step.op)}<span class="step-body"><code>${stepTextHtml(step.text)}</code>${limit}${heredoc}</span></li>`;
|
|
97
|
+
});
|
|
98
|
+
return `<ol class="activity-steps">${items.join("")}</ol>`;
|
|
28
99
|
}
|
|
@@ -23,11 +23,21 @@ export function noteHtml(note) {
|
|
|
23
23
|
return `<div class="note-line">${escapeHtml(from)}</div><div class="note-text">${escapeHtml(note?.content ?? "")}</div>`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Who a steer names as its sender: a plugin's own label, chi's sender ids
|
|
27
|
+
// in words (the TUI's Formatting#steer_sender agrees;
|
|
28
|
+
// spec/shared/labels_matrix.json). "": no one named.
|
|
29
|
+
const STEER_SENDERS = { parent_agent: "parent agent", chi_send: "chi send", plugin_send: "plugin" };
|
|
30
|
+
export function steerSender(source) {
|
|
31
|
+
const id = String(source ?? "");
|
|
32
|
+
return STEER_SENDERS[id] || id;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
// A plugin's steer (a nudge put into the running turn): one line that
|
|
27
36
|
// names who nudged the model, its text collapsed under it. +bubble+: on its
|
|
28
37
|
// own between bubbles (no step holds it), not a row of a step.
|
|
29
38
|
export function steerRowHtml({ source, text } = {}, { bubble = false } = {}) {
|
|
30
|
-
const
|
|
39
|
+
const sender = steerSender(source);
|
|
40
|
+
const who = sender ? `${sender} nudged the model` : "nudged the model";
|
|
31
41
|
return `<details class="${bubble ? "bubble " : ""}steer-row"><summary>${escapeHtml(who)}</summary>` +
|
|
32
42
|
`<div class="steer-text">${escapeHtml(text ?? "")}</div></details>`;
|
|
33
43
|
}
|
|
@@ -5,14 +5,20 @@
|
|
|
5
5
|
<title>Chi</title>
|
|
6
6
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
7
|
<style>
|
|
8
|
-
:root{
|
|
8
|
+
:root{color-scheme:dark;--bg:#0b0d11;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#7aa5ff;--accent-strong:#1f6feb;--accent-bg:#1b2336;--accent-ring:rgba(122,165,255,.18);--primary:#2a68d1;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#223150;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);--narr:color-mix(in srgb,var(--accent) 38%,var(--text-muted));
|
|
9
9
|
--glass-bg:color-mix(in srgb,var(--bg-panel) 70%,transparent);--glass-border:rgba(255,255,255,.08);--glass-highlight:rgba(255,255,255,.05);--glass-shadow:0 8px 30px rgba(0,0,0,.35);
|
|
10
|
-
--diff-add-bg:rgba(63,185,80,.14);--diff-add-text:#aff5b4;--diff-del-bg:rgba(248,81,73,.14);--diff-del-text:#ffc1bc
|
|
10
|
+
--diff-add-bg:rgba(63,185,80,.14);--diff-add-text:#aff5b4;--diff-del-bg:rgba(248,81,73,.14);--diff-del-text:#ffc1bc;
|
|
11
|
+
--card-hover-border:rgba(255,255,255,.14);--on-solid:#fff;--on-warn:#fff;--text-hover:#fff;--badge-bg:rgba(255,255,255,.08);--badge-ring:rgba(255,255,255,.18);--shadow-strong:rgba(0,0,0,.5);--backdrop:rgba(0,0,0,.85);--toast-shadow:0 6px 24px rgba(0,0,0,.25)}
|
|
12
|
+
/* Light palette: follows the system setting. :not([data-theme="dark"]) leaves room for a later in-page toggle (data-theme on <html>); there is none yet. Each palette sets color-scheme, so native radios, inputs and scrollbars follow it. */
|
|
13
|
+
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){color-scheme:light;--bg:#f5f6f8;--bg-panel:#ffffff;--bg-inset:#f1f3f6;--bg-raised:#e8ebf0;--bg-hover:#eceff4;--border:#dde1e7;--border-strong:#c3c9d2;--text:#1f2328;--text-bright:#0d1117;--text-2:#2f363d;--text-3:#475160;--text-muted:#57606a;--text-faint:#636c76;--accent:#0b5cd5;--accent-strong:#0550ae;--accent-bg:#e2ecfd;--accent-ring:rgba(11,92,213,.2);--primary:#1f6feb;--ok:#1a7f37;--ok-text:#116329;--ok-bg:#dafbe1;--warn:#9a6700;--warn-text:#7d4e00;--warn-bright:#875c00;--warn-bg:#fff6cc;--warn-border:#d4a72c;--danger:#cf222e;--danger-bg:#cf222e;--danger-text:#a40e26;--danger-bg-dim:#ffebe9;--think:#8250df;--think-text:#6639ba;--accent-border:#b8cef5;
|
|
14
|
+
--glass-border:rgba(31,35,40,.10);--glass-highlight:rgba(255,255,255,.7);--glass-shadow:0 8px 30px rgba(31,35,40,.10);
|
|
15
|
+
--diff-add-bg:rgba(26,127,55,.12);--diff-add-text:#116329;--diff-del-bg:rgba(207,34,46,.10);--diff-del-text:#82071e;
|
|
16
|
+
--card-hover-border:rgba(31,35,40,.18);--on-solid:#fff;--on-warn:#1f2328;--text-hover:#0d1117;--badge-bg:rgba(31,35,40,.06);--badge-ring:rgba(31,35,40,.15);--shadow-strong:rgba(31,35,40,.18);--backdrop:rgba(13,17,23,.85);--toast-shadow:0 6px 24px rgba(31,35,40,.14)}}
|
|
11
17
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
12
18
|
body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:radial-gradient(1100px 700px at 8% 0%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 70%),radial-gradient(900px 700px at 100% 100%,color-mix(in srgb,var(--think) 9%,transparent),transparent 70%),var(--bg);background-attachment:fixed;color:var(--text);height:100vh;display:flex;flex-direction:column}
|
|
13
19
|
/* Glass: the composer card, the strip cards, the all-sessions cards and the question cards (by selector, so app.js is untouched). */
|
|
14
20
|
#dock,.turn-stage,.dock-card,#topStrip .card,#allList .card,.bubble.question,.bubble.plugin-card{background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
|
|
15
|
-
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:
|
|
21
|
+
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:var(--card-hover-border)}
|
|
16
22
|
#topStrip .card.active,#allList .card.active{background:color-mix(in srgb,var(--accent-bg) 75%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
|
|
17
23
|
/* A session that waits on the user (a question, an approval, a card with actions): the warning colour of its question card, also when active or hovered. */
|
|
18
24
|
#topStrip .card.waiting,#allList .card.waiting{border-color:color-mix(in srgb,var(--warn) 75%,transparent)}
|
|
@@ -23,7 +29,7 @@ body.no-glass #dock,body.no-glass .turn-stage,body.no-glass .dock-card,body.no-g
|
|
|
23
29
|
body.no-glass .bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
24
30
|
body.no-glass .bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
25
31
|
button,input,textarea,select{font:inherit}
|
|
26
|
-
button{background:var(--primary);border:0;color
|
|
32
|
+
button{background:var(--primary);border:0;color:var(--on-solid);padding:8px 12px;border-radius:8px;cursor:pointer;transition:filter .12s ease,background .12s ease}
|
|
27
33
|
button:hover:not(:disabled){filter:brightness(1.12)}
|
|
28
34
|
button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
29
35
|
button.ghost{background:var(--bg-raised);color:var(--text-2)}
|
|
@@ -91,8 +97,8 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
91
97
|
.bubble.user.queued{border-left-style:dashed;opacity:.8}
|
|
92
98
|
.bubble.user.steered{border-left-style:dashed;border-left-color:var(--accent);opacity:.9}
|
|
93
99
|
.bubble.user.failed{border-left-color:var(--danger);opacity:.8}
|
|
94
|
-
.bubble.user .state-badge{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:2px 7px;border-radius:999px;background:
|
|
95
|
-
.bubble.user .state-badge::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.8;box-shadow:0 0 0 1px
|
|
100
|
+
.bubble.user .state-badge{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:2px 7px;border-radius:999px;background:var(--badge-bg);color:var(--text);font-size:11px;line-height:1.2;white-space:nowrap;flex-shrink:0}
|
|
101
|
+
.bubble.user .state-badge::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.8;box-shadow:0 0 0 1px var(--badge-ring)}
|
|
96
102
|
.bubble.user.queued .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
|
|
97
103
|
.bubble.user.steered .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
|
|
98
104
|
.bubble.user.failed .state-badge::before{background:var(--danger);box-shadow:0 0 0 1px color-mix(in srgb,var(--danger) 40%,transparent)}
|
|
@@ -141,6 +147,32 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
141
147
|
.activity-command-cwd{margin-bottom:2px}
|
|
142
148
|
.activity-command-cwd>span{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)}
|
|
143
149
|
.activity-command-cut{margin-top:2px}
|
|
150
|
+
.activity-command-desc{font-size:12px;line-height:1.4;color:var(--text-2);margin-bottom:3px;overflow-wrap:anywhere}
|
|
151
|
+
/* The command as its steps (CommandSteps): the op that links each to the one before, labels as small headings, limits and heredocs as chips; "raw" swaps in the command as written (CSS only). */
|
|
152
|
+
.activity-command.has-steps{background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;padding:4px 8px 5px}
|
|
153
|
+
.activity-command-head{display:flex;align-items:center;gap:8px;min-height:20px;padding-right:30px}
|
|
154
|
+
.activity-command-head.bare{float:right;min-height:0;margin-left:8px}
|
|
155
|
+
.has-steps .activity-command-cwd{flex:1 1 auto;min-width:0;margin:0;overflow-wrap:anywhere}
|
|
156
|
+
.activity-command-cwd>.activity-command-sep{font-family:inherit;color:var(--text-faint)}
|
|
157
|
+
.activity-command-raw{flex:none;display:inline-flex;align-items:center;height:18px;padding:0 7px;border:1px solid var(--border-strong);border-radius:9px;font-size:11px;color:var(--text-muted);cursor:pointer;user-select:none;-webkit-user-select:none}
|
|
158
|
+
.activity-command-raw:hover{color:var(--text-2)}
|
|
159
|
+
.activity-command-raw input{position:absolute;opacity:0;width:0;height:0;margin:0}
|
|
160
|
+
.activity-command-raw:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:1px}
|
|
161
|
+
.activity-command-raw:has(input:checked){color:var(--accent);border-color:var(--accent-border);background:var(--accent-tint)}
|
|
162
|
+
.activity-steps{list-style:none;margin:2px 0 0;padding:0;max-height:220px;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;color:var(--text-2)}
|
|
163
|
+
.activity-step{display:flex;align-items:baseline;gap:6px;min-width:0}
|
|
164
|
+
.step-body{flex:1 1 auto;min-width:0;display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 6px}
|
|
165
|
+
.step-body>code{min-width:0;font:inherit;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
166
|
+
.step-op{flex:none;width:2.4em;text-align:right;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;color:var(--text-faint)}
|
|
167
|
+
.step-op.else{color:var(--warn-text)}
|
|
168
|
+
.step-label{margin:5px 0 0 calc(2.4em + 6px);font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;font-weight:600;color:var(--text-muted)}
|
|
169
|
+
.step-label:first-child{margin-top:1px}
|
|
170
|
+
.step-chip{flex:none;align-self:center;padding:0 6px;border:1px solid var(--border-strong);border-radius:8px;font-family:ui-sans-serif,system-ui,sans-serif;font-size:10.5px;line-height:15px;color:var(--text-muted);white-space:nowrap}
|
|
171
|
+
.step-chip.heredoc{border-style:dashed}
|
|
172
|
+
.step-plumbing{color:var(--text-faint)}
|
|
173
|
+
.activity-command.has-steps>pre{display:none;max-height:220px;margin-top:2px;padding:0 4px;background:none;border:0}
|
|
174
|
+
.has-steps:has(.activity-command-raw input:checked)>pre{display:block}
|
|
175
|
+
.has-steps:has(.activity-command-raw input:checked)>.activity-steps{display:none}
|
|
144
176
|
@media(max-width:600px){
|
|
145
177
|
.activity-params{flex:1 1 0}
|
|
146
178
|
.activity-output{margin-left:0}
|
|
@@ -341,7 +373,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
341
373
|
/* The selection bubble (app.js Annotate): Annotate + the presets (web.annotate_presets), small pills with a gap (they wrap on a narrow page). */
|
|
342
374
|
#annotateBar{position:fixed;z-index:20;display:flex;flex-wrap:wrap;gap:4px;max-width:calc(100vw - 16px)}
|
|
343
375
|
#annotateBar[hidden]{display:none}
|
|
344
|
-
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px
|
|
376
|
+
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px var(--shadow-strong)}
|
|
345
377
|
#annotateBar .annotate-preset{background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong);padding:3px 10px;max-width:32ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
346
378
|
/* Copy buttons (copy.js): top-right in an answer and a code block, left of a prompt (its right edge is the page's); shown on hover or focus, always (dim) on touch. They hold no text: "copied ✓" is CSS content. */
|
|
347
379
|
.bubble.output,.bubble.user{position:relative}
|
|
@@ -382,7 +414,7 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
382
414
|
.thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
383
415
|
.thumb{max-height:120px;max-width:200px;border-radius:6px;border:1px solid var(--border-strong);cursor:zoom-in;object-fit:contain;background:var(--bg)}
|
|
384
416
|
.activity-row .thumbs{flex-basis:100%}
|
|
385
|
-
#lightbox{position:fixed;inset:0;z-index:50;background:
|
|
417
|
+
#lightbox{position:fixed;inset:0;z-index:50;background:var(--backdrop);display:flex;align-items:center;justify-content:center;cursor:zoom-out}
|
|
386
418
|
#lightbox[hidden]{display:none}
|
|
387
419
|
#lightbox img{max-width:96vw;max-height:94vh;border-radius:6px}
|
|
388
420
|
#composer textarea{flex:1 1 auto;min-height:72px;resize:none;background:transparent;border:0;color:var(--text);padding:2px;border-radius:0;font-size:15px;line-height:1.45}
|
|
@@ -573,7 +605,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
573
605
|
#infoBar .meta{font-size:11px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
|
574
606
|
#infoBar .meta .id{font-family:ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--accent)}
|
|
575
607
|
#infoBar .meta .copy-attach{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong)}
|
|
576
|
-
#infoBar .meta .copy-attach:hover{color
|
|
608
|
+
#infoBar .meta .copy-attach:hover{color:var(--text-hover);border-color:var(--border-strong)}
|
|
577
609
|
#infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
|
|
578
610
|
#infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);word-break:break-all}
|
|
579
611
|
/* The footer sits on the card's glass: its chips are tinted, not solid, so the glass reads through them. */
|
|
@@ -585,7 +617,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
585
617
|
#infoBar .meta .worker-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:2px 2px 2px 6px;border-radius:6px;background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border);white-space:nowrap}
|
|
586
618
|
#infoBar .meta .worker-badge:not(:has(button)){padding-right:6px}
|
|
587
619
|
#infoBar .meta .worker-restart{padding:1px 8px;border-radius:5px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
588
|
-
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color
|
|
620
|
+
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}
|
|
589
621
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
590
622
|
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;line-height:1.4;max-height:2.8em}
|
|
591
623
|
#infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
|
|
@@ -596,7 +628,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
596
628
|
#infoBar #infoArchiveBtn{background:transparent;color:var(--text-muted);border:1px solid var(--border-strong)}
|
|
597
629
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
598
630
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
599
|
-
#infoBar #infoDeleteBtn:hover{color
|
|
631
|
+
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
600
632
|
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable) and the first-message line, the strip card's title, goes. */
|
|
601
633
|
@media(max-width:600px){
|
|
602
634
|
/* The key hint: cut off at this width, and phones have no Shift+Enter. */
|
|
@@ -718,7 +750,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
718
750
|
.card.archived{opacity:.6}
|
|
719
751
|
.card.archived:hover{opacity:.85}
|
|
720
752
|
.card .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
721
|
-
#toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:
|
|
753
|
+
#toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
722
754
|
#toast[hidden]{display:none}
|
|
723
755
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
724
756
|
#toast .toast-close{background:transparent;border:0;color:var(--text-muted);cursor:pointer;padding:2px 4px;margin-left:-6px}
|
|
@@ -736,7 +768,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
736
768
|
#allList .card{position:relative}
|
|
737
769
|
.card-del{position:absolute;top:6px;right:6px;padding:2px 7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);font-size:12px;line-height:1.2;cursor:pointer;opacity:0;transition:opacity .15s}
|
|
738
770
|
#allList .card:hover .card-del, .card-del:focus-visible{opacity:1}
|
|
739
|
-
.card-del:hover{color
|
|
771
|
+
.card-del:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
740
772
|
@media (hover:none){.card-del{opacity:1}}
|
|
741
773
|
.card.active{background:var(--accent-bg);border-color:var(--accent-border)}
|
|
742
774
|
.card .preview{font-size:14px;line-height:1.4;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// covers the rules; stage_view.js draws them.
|
|
5
5
|
import { toolName } from "./activity.js";
|
|
6
6
|
import { splitSentences } from "./sentences.js";
|
|
7
|
+
import { steerSender } from "./format.js";
|
|
7
8
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
8
9
|
|
|
9
10
|
const READ_LIKE = new Set(["read", "memory_read", "web_fetch", "task_get", "task_list", "list_sessions"]);
|
|
@@ -174,7 +175,10 @@ export function handOffOrder({ prompt = null, block = null, extras = [], answer
|
|
|
174
175
|
// chi's own asking-again row (a notice with a line and no hook) does not:
|
|
175
176
|
// the status line tells that already. null: nothing flashes.
|
|
176
177
|
export function flashOf(what, data = {}) {
|
|
177
|
-
if (what === "steer")
|
|
178
|
+
if (what === "steer") {
|
|
179
|
+
const sender = steerSender(data.source);
|
|
180
|
+
return { kind: "steer", text: sender ? `${sender} nudged the model` : "nudged the model" };
|
|
181
|
+
}
|
|
178
182
|
if (what !== "notice" || !data.hook) return null;
|
|
179
183
|
return { kind: data.level === "warn" ? "warn" : "info", text: data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}` };
|
|
180
184
|
}
|
|
@@ -234,7 +234,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
234
234
|
name.textContent = tool.name;
|
|
235
235
|
const title = node("span", "ts-title", now);
|
|
236
236
|
title.textContent = tool.title;
|
|
237
|
-
// The full command when the call has one (the title is its first
|
|
237
|
+
// The full command when the call has one (the title is its first step +N).
|
|
238
238
|
title.title = tool.command || tool.title;
|
|
239
239
|
} else {
|
|
240
240
|
const word = node("span", "ts-title quiet", now);
|
|
@@ -54,8 +54,8 @@ function fillActivityRow(el, row, sessionId) {
|
|
|
54
54
|
const outEl = el.querySelector(".activity-output");
|
|
55
55
|
// The call's full command, above its output (running too).
|
|
56
56
|
if (row.view?.command && !el.querySelector(".activity-command")) {
|
|
57
|
-
if (outEl) outEl.insertAdjacentHTML("beforebegin", commandBlockHtml(row.view));
|
|
58
|
-
else el.insertAdjacentHTML("beforeend", commandBlockHtml(row.view));
|
|
57
|
+
if (outEl) outEl.insertAdjacentHTML("beforebegin", commandBlockHtml(row.view, row.title));
|
|
58
|
+
else el.insertAdjacentHTML("beforeend", commandBlockHtml(row.view, row.title));
|
|
59
59
|
}
|
|
60
60
|
if (outEl) {
|
|
61
61
|
if (row.status === "running") {
|
|
@@ -691,7 +691,7 @@ function reloadRowHtml(r, thumbs) {
|
|
|
691
691
|
: "";
|
|
692
692
|
return `<div class="activity-row" data-key="${escapeHtml(r.key)}"><span class="activity-status ${activityStatusClass(r.status)}">${activityStatusLabel(r.status)}</span>` +
|
|
693
693
|
`<span class="activity-tool">${escapeHtml(toolName(r))}</span>${params}${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}` +
|
|
694
|
-
`${commandBlockHtml(r.view)}${output}` +
|
|
694
|
+
`${commandBlockHtml(r.view, r.title)}${output}` +
|
|
695
695
|
`${r.images?.length ? thumbs(r.images) : ""}${r.diff ? activityDiffHtml(r.diff) : ""}</div>`;
|
|
696
696
|
}
|
|
697
697
|
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -112,6 +112,9 @@ module Samagotchi
|
|
|
112
112
|
# stream gets the session's history and status in its snapshot, not
|
|
113
113
|
# an empty session until the first turn.
|
|
114
114
|
@engine.session = @session
|
|
115
|
+
# Input already waiting in the inbox starts the next turn at once: no
|
|
116
|
+
# turn-end warm-up then.
|
|
117
|
+
@engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? }
|
|
115
118
|
@turn_flow = TurnFlow.new(engine: @engine)
|
|
116
119
|
# Its question's answer is queued as the matching /continue (the
|
|
117
120
|
# Bridge exists by the time anyone answers).
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -130,7 +130,9 @@ files:
|
|
|
130
130
|
- docs/hooks.md
|
|
131
131
|
- docs/internals/background-tasks.md
|
|
132
132
|
- docs/internals/context-telemetry.md
|
|
133
|
+
- docs/internals/engine.md
|
|
133
134
|
- docs/internals/gemma4-contract.md
|
|
135
|
+
- docs/internals/prompt-caching.md
|
|
134
136
|
- docs/internals/tool-guardrails.md
|
|
135
137
|
- docs/memory.md
|
|
136
138
|
- docs/plugins.md
|
|
@@ -196,6 +198,7 @@ files:
|
|
|
196
198
|
- lib/samagotchi/cli/flags.rb
|
|
197
199
|
- lib/samagotchi/cli/parent_wait.rb
|
|
198
200
|
- lib/samagotchi/client.rb
|
|
201
|
+
- lib/samagotchi/command_steps.rb
|
|
199
202
|
- lib/samagotchi/commands/registry.rb
|
|
200
203
|
- lib/samagotchi/config.rb
|
|
201
204
|
- lib/samagotchi/config_text_edit.rb
|
|
@@ -322,6 +325,7 @@ files:
|
|
|
322
325
|
- lib/samagotchi/prompt.rb
|
|
323
326
|
- lib/samagotchi/prompt_history.rb
|
|
324
327
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
328
|
+
- lib/samagotchi/prompt_warmup.rb
|
|
325
329
|
- lib/samagotchi/question_desk.rb
|
|
326
330
|
- lib/samagotchi/recap_setup.rb
|
|
327
331
|
- lib/samagotchi/recap_store.rb
|
|
@@ -373,6 +377,7 @@ files:
|
|
|
373
377
|
- lib/samagotchi/terminal_ui/version_lines.rb
|
|
374
378
|
- lib/samagotchi/text_diff.rb
|
|
375
379
|
- lib/samagotchi/thinking.rb
|
|
380
|
+
- lib/samagotchi/thinking_tails.rb
|
|
376
381
|
- lib/samagotchi/thinking_ticker.rb
|
|
377
382
|
- lib/samagotchi/thought_stream_splitter.rb
|
|
378
383
|
- lib/samagotchi/token_usage.rb
|
|
@@ -402,6 +407,7 @@ files:
|
|
|
402
407
|
- lib/samagotchi/tools/register_reminder.rb
|
|
403
408
|
- lib/samagotchi/tools/registry.rb
|
|
404
409
|
- lib/samagotchi/tools/send_note.rb
|
|
410
|
+
- lib/samagotchi/tools/shell.rb
|
|
405
411
|
- lib/samagotchi/tools/task_create.rb
|
|
406
412
|
- lib/samagotchi/tools/task_get.rb
|
|
407
413
|
- lib/samagotchi/tools/task_list.rb
|