samagotchi 0.23.0 → 0.24.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 +40 -1
- data/docs/cli.md +6 -1
- data/docs/configuration.md +9 -0
- data/docs/hooks.md +20 -6
- data/docs/plugins.md +1 -1
- data/docs/sub-agent.md +4 -3
- data/lib/samagotchi/bridge/card_store.rb +4 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -0
- data/lib/samagotchi/bridge.rb +7 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +4 -0
- data/lib/samagotchi/engine.rb +80 -1
- data/lib/samagotchi/generation_phase.rb +83 -0
- data/lib/samagotchi/guardrails/shell_lex.rb +111 -3
- data/lib/samagotchi/guardrails/shell_paths.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +4 -2
- data/lib/samagotchi/kernel_loop.rb +19 -9
- data/lib/samagotchi/llm/chat_loop.rb +20 -10
- data/lib/samagotchi/llm/openai_chat.rb +3 -0
- data/lib/samagotchi/log_subscriber.rb +3 -2
- data/lib/samagotchi/prompt.rb +4 -1
- data/lib/samagotchi/steer.rb +103 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +9 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
- data/lib/samagotchi/terminal_ui.rb +5 -0
- data/lib/samagotchi/thought_stream_splitter.rb +9 -3
- data/lib/samagotchi/tool_runner.rb +7 -0
- data/lib/samagotchi/tool_view.rb +40 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/waiting_steer.rb +64 -0
- data/lib/samagotchi/web/message_parts.rb +5 -0
- data/lib/samagotchi/web/public/activity.js +5 -0
- data/lib/samagotchi/web/public/app.js +2 -1
- data/lib/samagotchi/web/public/command_view.js +28 -0
- data/lib/samagotchi/web/public/copy.js +7 -0
- data/lib/samagotchi/web/public/index.html +8 -0
- data/lib/samagotchi/web/public/stage_model.js +11 -4
- data/lib/samagotchi/web/public/stage_view.js +5 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -6
- data/lib/samagotchi/worker.rb +4 -1
- metadata +5 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A message for the running turn that may cut its generation but came too
|
|
5
|
+
# early (the generation hadn't streamed only thinking for long enough):
|
|
6
|
+
# it waits here until the thinking passes steer.cut_after (the Engine
|
|
7
|
+
# checks on each thinking chunk), or until a drain hands the message to
|
|
8
|
+
# the model at a boundary, or the turn ends. One cut covers every waiting
|
|
9
|
+
# message: the first one's source names it.
|
|
10
|
+
#
|
|
11
|
+
# The epoch counts the drains that took input. A caller reads it before
|
|
12
|
+
# it queues its message; a drain after that may have taken the message
|
|
13
|
+
# already, so the message doesn't wait (it may be in the model's prompt).
|
|
14
|
+
# Written from the UIs' threads and the turn's; its own Mutex.
|
|
15
|
+
class WaitingSteer
|
|
16
|
+
def initialize
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@source = nil
|
|
19
|
+
@epoch = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] the drains that took input so far
|
|
23
|
+
def epoch
|
|
24
|
+
@mutex.synchronize { @epoch }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# +source+'s message waits for a cut, unless a drain took input since
|
|
28
|
+
# +epoch+. Returns whether it waits.
|
|
29
|
+
def wait!(source, epoch: nil)
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
next false if epoch && epoch != @epoch
|
|
32
|
+
|
|
33
|
+
@source ||= source.to_s
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def waiting?
|
|
39
|
+
@mutex.synchronize { !@source.nil? }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The waiting source, taken (nil when none waits).
|
|
43
|
+
def take
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
source = @source
|
|
46
|
+
@source = nil
|
|
47
|
+
source
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A drain took input: what waited is delivered.
|
|
52
|
+
def delivered!
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
@epoch += 1
|
|
55
|
+
@source = nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The turn ended, or a cut happened: nothing waits.
|
|
60
|
+
def clear!
|
|
61
|
+
@mutex.synchronize { @source = nil }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../model_profile"
|
|
4
4
|
require_relative "../tool_call_parser"
|
|
5
5
|
require_relative "../tool_activity"
|
|
6
|
+
require_relative "../tool_view"
|
|
6
7
|
require_relative "../llm/native_tool_normalizer"
|
|
7
8
|
require_relative "../tools/builtins"
|
|
8
9
|
require_relative "../image_store"
|
|
@@ -158,6 +159,10 @@ module Samagotchi
|
|
|
158
159
|
part = { tool: name, params: params.to_s }
|
|
159
160
|
title = ToolActivity.tool_title(name, call, cwd: cwd)
|
|
160
161
|
part[:title] = title if title
|
|
162
|
+
# Built from the saved call (never saved itself), so older sessions
|
|
163
|
+
# get it too.
|
|
164
|
+
view = ToolView.for(name, call)&.to_h
|
|
165
|
+
part[:view] = view if view
|
|
161
166
|
part[:label] = label if label.is_a?(String) && !label.empty?
|
|
162
167
|
unless output.nil?
|
|
163
168
|
part[:output] = output.length > OUTPUT_MAX ? output[0, OUTPUT_MAX] : output
|
|
@@ -50,6 +50,7 @@ export function addStarted(model, event = {}) {
|
|
|
50
50
|
row.label = event.label || row.label;
|
|
51
51
|
row.params = event.params || row.params;
|
|
52
52
|
row.title = event.title || row.title;
|
|
53
|
+
row.view = event.view || row.view;
|
|
53
54
|
return { row, created: false };
|
|
54
55
|
}
|
|
55
56
|
row = {
|
|
@@ -63,6 +64,9 @@ export function addStarted(model, event = {}) {
|
|
|
63
64
|
// project-relative path, a command without its "cd … &&"); rows show it
|
|
64
65
|
// in place of the params when there is one.
|
|
65
66
|
title: event.title || null,
|
|
67
|
+
// The call's full arguments for the expanded row and the hovers (the
|
|
68
|
+
// server's ToolView: an execute/task_create's command, its cwd).
|
|
69
|
+
view: event.view || null,
|
|
66
70
|
status: ROW_STATUS.RUNNING,
|
|
67
71
|
output: "",
|
|
68
72
|
output_truncated: false,
|
|
@@ -87,6 +91,7 @@ export function addCompleted(model, event = {}) {
|
|
|
87
91
|
label: event.label,
|
|
88
92
|
params: event.activity?.params || "",
|
|
89
93
|
title: event.activity?.title,
|
|
94
|
+
view: event.view,
|
|
90
95
|
});
|
|
91
96
|
row = synthesized.row;
|
|
92
97
|
}
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, mergeTiming, normalizeTiming, turnTimingText,
|
|
46
46
|
} from "./timing.js";
|
|
47
47
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
48
|
-
import { clientLabel, commandView, displayWait, dropEarlyRestores, emptyAnswerLine, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
48
|
+
import { clientLabel, commandView, displayWait, dropEarlyRestores, emptyAnswerLine, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
49
49
|
|
|
50
50
|
const $ = (s) => document.querySelector(s);
|
|
51
51
|
const topStripEl = $("#topStrip");
|
|
@@ -1861,6 +1861,7 @@ const streamHandlers = {
|
|
|
1861
1861
|
// The loop asks again after an empty answer: a row of the empty step
|
|
1862
1862
|
// (a reload puts it back from the snapshot's cards).
|
|
1863
1863
|
empty_answer_retry: (data) => { turnDom.notice({ line: emptyRetryLine(data) }); },
|
|
1864
|
+
steer_cut: (data) => { turnDom.notice({ line: steerCutLine(data) }); },
|
|
1864
1865
|
// The provider is asked again after an error, or the turn waits for
|
|
1865
1866
|
// plugins' setup: a note on the live timing line until it streams.
|
|
1866
1867
|
generation_retrying: (data) => setLiveTurnNote(retryStatusLine(data)),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A tool call's full command in a richer UI than the TUI's params line:
|
|
2
|
+
// the server's view (ToolView: an execute/task_create's command uncut up to
|
|
3
|
+
// 8,000 chars, the call's cwd: argument). Pure HTML builders, shared by the
|
|
4
|
+
// live row (turn_view.js fillActivityRow) and the reloaded one
|
|
5
|
+
// (reloadRowHtml), so the two can't differ.
|
|
6
|
+
import { copyButtonHtml } from "./copy.js";
|
|
7
|
+
import { escapeHtml } from "./format.js";
|
|
8
|
+
|
|
9
|
+
// What a row's title hovers: the full command when the call has one,
|
|
10
|
+
// else the params line when the row shows a title in its place.
|
|
11
|
+
export function rowHover(row) {
|
|
12
|
+
if (row?.view?.command) return row.view.command;
|
|
13
|
+
return row?.title && row?.params ? row.params : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The expanded row's command block: "in <cwd>" when the call gave one (as
|
|
17
|
+
// given: execute's may be relative to the project root), the command as
|
|
18
|
+
// written (whitespace kept, its own scroll), a copy button, and a line
|
|
19
|
+
// saying how much a cut command left out. "" for a call without a view.
|
|
20
|
+
export function commandBlockHtml(view) {
|
|
21
|
+
const command = view?.command;
|
|
22
|
+
if (!command) return "";
|
|
23
|
+
const cwd = view.cwd ? `<div class="activity-command-cwd">in <span>${escapeHtml(view.cwd)}</span></div>` : "";
|
|
24
|
+
const cut = view.truncated
|
|
25
|
+
? `<div class="activity-command-cut">… (${command.length.toLocaleString("en-US")} of ${Number(view.chars || 0).toLocaleString("en-US")} chars)</div>`
|
|
26
|
+
: "";
|
|
27
|
+
return `<div class="activity-command code-wrap">${cwd}<pre><code>${escapeHtml(command)}</code></pre>${copyButtonHtml("code")}${cut}</div>`;
|
|
28
|
+
}
|
|
@@ -88,6 +88,13 @@ const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"
|
|
|
88
88
|
const BUBBLES = ".bubble.output:not(.streaming), .bubble.user";
|
|
89
89
|
const CODE = ".bubble.output pre";
|
|
90
90
|
|
|
91
|
+
// The button as markup, for a builder that returns HTML (a tool row's
|
|
92
|
+
// command block, command_view.js); the same click handler answers it.
|
|
93
|
+
export function copyButtonHtml(kind) {
|
|
94
|
+
const title = kind === "code" ? "Copy code" : "Copy";
|
|
95
|
+
return `<button type="button" class="copy-btn copy-${kind}" title="${title}" aria-label="${title}">${ICON}</button>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
function makeButton(doc, kind) {
|
|
92
99
|
const btn = doc.createElement("button");
|
|
93
100
|
btn.type = "button";
|
|
@@ -134,10 +134,18 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
134
134
|
.activity-diff>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
135
135
|
.activity-diff>summary:hover{color:var(--text-2)}
|
|
136
136
|
.activity-diff[open]>summary{margin-bottom:4px}
|
|
137
|
+
/* An execute/task_create's full command, above its output and lined up with it: as written, its own scroll. */
|
|
138
|
+
.activity-row>.activity-command{flex:1 1 100%;min-width:0;margin:4px 0 2px 60px}
|
|
139
|
+
.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}
|
|
140
|
+
.activity-command-cwd,.activity-command-cut{font-size:11px;color:var(--text-faint)}
|
|
141
|
+
.activity-command-cwd{margin-bottom:2px}
|
|
142
|
+
.activity-command-cwd>span{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)}
|
|
143
|
+
.activity-command-cut{margin-top:2px}
|
|
137
144
|
@media(max-width:600px){
|
|
138
145
|
.activity-params{flex:1 1 0}
|
|
139
146
|
.activity-output{margin-left:0}
|
|
140
147
|
.activity-diff{margin-left:0}
|
|
148
|
+
.activity-row>.activity-command{margin-left:0}
|
|
141
149
|
}
|
|
142
150
|
.bubble.thinking{border-left:3px solid var(--think);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));padding:8px 12px}
|
|
143
151
|
.bubble.thinking:not([open]),.bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
|
|
@@ -78,6 +78,12 @@ function rowTitle(row) {
|
|
|
78
78
|
return row.title || row.params || "";
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// A slot's full command (the row's view), for its hover; none for a tool
|
|
82
|
+
// without one.
|
|
83
|
+
function withCommand(slot, row) {
|
|
84
|
+
return row.view?.command ? { ...slot, command: row.view.command } : slot;
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
// While the first step has no thinking and no tool rows the turn is plain:
|
|
82
88
|
// its text streams in the stage's answer slot, as the turn view's
|
|
83
89
|
// chrome-less block streams like a bubble.
|
|
@@ -98,9 +104,10 @@ const NO_ANSWER = "no answer";
|
|
|
98
104
|
// headline the newest narration sentence (+narration+, the view's sentence
|
|
99
105
|
// feed), else the newest thinking one (+thinking+, its ticker),
|
|
100
106
|
// else "…";
|
|
101
|
-
// tool the running call ({name, title, kind}
|
|
107
|
+
// tool the running call ({name, title, kind}, and command: its full
|
|
108
|
+
// command when it has one) or null;
|
|
102
109
|
// trail the last three finished calls, newest last (a file's title
|
|
103
|
-
// without its dir);
|
|
110
|
+
// without its dir; command as for tool);
|
|
104
111
|
// ticks one per call: {kind, status, step (the gen's index)}.
|
|
105
112
|
// +pending+: a card waits for the user; +ended+: the turn's end kind;
|
|
106
113
|
// +emptyAnswer+: a completed turn ended with no answer
|
|
@@ -123,13 +130,13 @@ export function liveSlots(turn, { narration = "", thinking = "", pending = false
|
|
|
123
130
|
else if (thinking) headline = { text: thinking, thinking: true };
|
|
124
131
|
const trail = rows.filter(({ row }) => row.status !== "running").slice(-TRAIL).map(({ row }) => {
|
|
125
132
|
const title = rowTitle(row);
|
|
126
|
-
return { name: toolName(row), title: FILE_TOOLS.has(row.tool) ? title.split("/").pop() : title, status: row.status };
|
|
133
|
+
return withCommand({ name: toolName(row), title: FILE_TOOLS.has(row.tool) ? title.split("/").pop() : title, status: row.status }, row);
|
|
127
134
|
});
|
|
128
135
|
return {
|
|
129
136
|
phase,
|
|
130
137
|
step: ended ? gens.filter((g) => !g.answer || g.thinking || g.tools.length).length : gens.length,
|
|
131
138
|
headline,
|
|
132
|
-
tool: running ? { name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) } : null,
|
|
139
|
+
tool: running ? withCommand({ name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) }, running) : null,
|
|
133
140
|
trail,
|
|
134
141
|
ticks: rows.map(({ row, step }) => ({ kind: toolKind(row.tool), status: row.status, step })),
|
|
135
142
|
};
|
|
@@ -195,6 +195,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
195
195
|
const elapsed = endKind ? formatDuration(endedAt - startedAt) : "";
|
|
196
196
|
chipLabel.textContent = `${slots.step} step${slots.step === 1 ? "" : "s"} · ${calls} tool call${calls === 1 ? "" : "s"}${elapsed ? ` · ${elapsed}` : ""}`;
|
|
197
197
|
miniTool.textContent = collapsed && slots.tool ? `${slots.tool.name} ${slots.tool.title}` : "";
|
|
198
|
+
miniTool.title = (collapsed && slots.tool?.command) || "";
|
|
198
199
|
|
|
199
200
|
live.hidden = plain || !!endKind;
|
|
200
201
|
cloud.hidden = !blockEl || blockEl.parentNode !== cloudBody;
|
|
@@ -222,7 +223,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
222
223
|
|
|
223
224
|
function drawNow(tool, gen) {
|
|
224
225
|
const quiet = gen?.text && !gen.textDone ? "writing…" : "thinking…";
|
|
225
|
-
const key = tool ? `run|${tool.name}|${tool.title}` : `quiet|${quiet}`;
|
|
226
|
+
const key = tool ? `run|${tool.name}|${tool.title}|${tool.command || ""}` : `quiet|${quiet}`;
|
|
226
227
|
if (shown.now === key) return;
|
|
227
228
|
shown.now = key;
|
|
228
229
|
now.replaceChildren();
|
|
@@ -233,7 +234,8 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
233
234
|
name.textContent = tool.name;
|
|
234
235
|
const title = node("span", "ts-title", now);
|
|
235
236
|
title.textContent = tool.title;
|
|
236
|
-
title
|
|
237
|
+
// The full command when the call has one (the title is its first line).
|
|
238
|
+
title.title = tool.command || tool.title;
|
|
237
239
|
} else {
|
|
238
240
|
const word = node("span", "ts-title quiet", now);
|
|
239
241
|
word.textContent = quiet;
|
|
@@ -251,6 +253,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
251
253
|
const failed = item.status === "error";
|
|
252
254
|
const el = node("span", `ts-trail-item${failed ? " error" : ""}`, trail);
|
|
253
255
|
el.textContent = `${failed ? "✕" : "✓"} ${item.name} ${item.title}`.trim();
|
|
256
|
+
if (item.command) el.title = item.command;
|
|
254
257
|
}
|
|
255
258
|
if (!flashing && !items.length) node("span", "ts-trail-item none", trail).textContent = "no tool calls yet";
|
|
256
259
|
}
|
|
@@ -123,6 +123,17 @@ export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy } = {}) {
|
|
|
123
123
|
return `↻ ${what}, asking again (${attempt}/${of})`;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// The row a step gets when a message for the running turn cut a generation
|
|
127
|
+
// that was only thinking (steer_cut; source "" is the user's, as is an
|
|
128
|
+
// unknown one). The TUI words it the same (spec/shared/labels_matrix.json).
|
|
129
|
+
const STEER_CUT_FOR = {
|
|
130
|
+
chi_send: "a message sent with chi send",
|
|
131
|
+
parent_agent: "the parent agent's message",
|
|
132
|
+
};
|
|
133
|
+
export function steerCutLine({ source } = {}) {
|
|
134
|
+
return `↪ cut in for ${STEER_CUT_FOR[source] || "your message"}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
// The one muted line for a turn that ended with no answer (turn_completed's
|
|
127
138
|
// turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
|
|
128
139
|
// it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
|
|
@@ -136,6 +147,7 @@ export function emptyAnswerLine({ retries = 0 } = {}) {
|
|
|
136
147
|
// notice as "<label>: text", the loop's asking-again row as its line.
|
|
137
148
|
export function noticeLine(entry = {}) {
|
|
138
149
|
if (entry.type === "empty_answer_retry") return emptyRetryLine(entry);
|
|
150
|
+
if (entry.type === "steer_cut") return steerCutLine(entry);
|
|
139
151
|
return `${hookNoticeLabel(entry.hook)}: ${entry.text || ""}`;
|
|
140
152
|
}
|
|
141
153
|
|
|
@@ -451,7 +463,8 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
451
463
|
const call = { iteration: part.iteration, call_index: part.call_index, tool: part.tool };
|
|
452
464
|
if (part.label) call.label = part.label;
|
|
453
465
|
const title = part.title ? { title: part.title } : {};
|
|
454
|
-
|
|
466
|
+
const view = part.view ? { view: part.view } : {};
|
|
467
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...view });
|
|
455
468
|
if (part.status !== "running") {
|
|
456
469
|
// The activity as the live event had it; action and duration_ms
|
|
457
470
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -463,6 +476,7 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
463
476
|
};
|
|
464
477
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
465
478
|
if (part.diff) completed.diff = part.diff;
|
|
479
|
+
if (part.view) completed.view = part.view;
|
|
466
480
|
events.push(withImages(completed, part.images));
|
|
467
481
|
}
|
|
468
482
|
break;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { toolName } from "./activity.js";
|
|
13
13
|
import { leavesBlock } from "./card.js";
|
|
14
14
|
import { copySourceAttr } from "./copy.js";
|
|
15
|
+
import { commandBlockHtml, rowHover } from "./command_view.js";
|
|
15
16
|
import { activityDiffHtml } from "./diff_view.js";
|
|
16
17
|
import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
|
|
17
18
|
import { thumbsHtml } from "./images.js";
|
|
@@ -45,11 +46,17 @@ function fillActivityRow(el, row, sessionId) {
|
|
|
45
46
|
if (toolEl) toolEl.textContent = toolName(row);
|
|
46
47
|
const paramsEl = el.querySelector(".activity-params");
|
|
47
48
|
if (paramsEl) {
|
|
48
|
-
// The title in place of the params; the full
|
|
49
|
+
// The title in place of the params; the full command (else the full
|
|
50
|
+
// params line) on hover.
|
|
49
51
|
paramsEl.textContent = row.title || row.params || "";
|
|
50
|
-
paramsEl.title = row
|
|
52
|
+
paramsEl.title = rowHover(row);
|
|
51
53
|
}
|
|
52
54
|
const outEl = el.querySelector(".activity-output");
|
|
55
|
+
// The call's full command, above its output (running too).
|
|
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));
|
|
59
|
+
}
|
|
53
60
|
if (outEl) {
|
|
54
61
|
if (row.status === "running") {
|
|
55
62
|
outEl.style.display = "none";
|
|
@@ -667,14 +674,15 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
667
674
|
};
|
|
668
675
|
}
|
|
669
676
|
|
|
670
|
-
// A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params,
|
|
671
|
-
// 300 chars, the rest on hover), plus the call's
|
|
677
|
+
// A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params, the full command (an
|
|
678
|
+
// execute/task_create's view), output (its "[tool]" tag dropped, 300 chars, the rest on hover), plus the call's
|
|
679
|
+
// duration from its record.
|
|
672
680
|
// Params, output and the call's images come from the saved messages'
|
|
673
681
|
// parts; without them the row has the record's name, status and duration
|
|
674
682
|
// only. +thumbs(images)+ draws the images, as the live row does.
|
|
675
683
|
function reloadRowHtml(r, thumbs) {
|
|
676
684
|
const shown = r.title || r.params;
|
|
677
|
-
const hover = r
|
|
685
|
+
const hover = rowHover(r) ? ` title="${escapeHtml(rowHover(r))}"` : "";
|
|
678
686
|
const params = shown ? `<span class="activity-params"${hover}>${escapeHtml(shown)}</span>` : "";
|
|
679
687
|
const duration = formatDuration(r.duration_ms);
|
|
680
688
|
const full = String(r.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
@@ -682,7 +690,8 @@ function reloadRowHtml(r, thumbs) {
|
|
|
682
690
|
? `<div class="activity-output" title="${escapeHtml(full)}">${escapeHtml(full.length > 300 ? `${full.slice(0, 300)}…` : full)}</div>`
|
|
683
691
|
: "";
|
|
684
692
|
return `<div class="activity-row" data-key="${escapeHtml(r.key)}"><span class="activity-status ${activityStatusClass(r.status)}">${activityStatusLabel(r.status)}</span>` +
|
|
685
|
-
`<span class="activity-tool">${escapeHtml(toolName(r))}</span>${params}${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}
|
|
693
|
+
`<span class="activity-tool">${escapeHtml(toolName(r))}</span>${params}${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}` +
|
|
694
|
+
`${commandBlockHtml(r.view)}${output}` +
|
|
686
695
|
`${r.images?.length ? thumbs(r.images) : ""}${r.diff ? activityDiffHtml(r.diff) : ""}</div>`;
|
|
687
696
|
}
|
|
688
697
|
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "session"
|
|
|
7
7
|
require_relative "session_inbox"
|
|
8
8
|
require_relative "turn_note"
|
|
9
9
|
require_relative "context_note"
|
|
10
|
+
require_relative "steer"
|
|
10
11
|
require_relative "worker_idle_exit"
|
|
11
12
|
require_relative "session_manager"
|
|
12
13
|
require_relative "archive_store"
|
|
@@ -571,7 +572,9 @@ module Samagotchi
|
|
|
571
572
|
@engine.announce(type: :input_merged, count: merged.size, origins: merged.filter_map(&:last))
|
|
572
573
|
@merged_this_turn.concat(merged)
|
|
573
574
|
end
|
|
574
|
-
|
|
575
|
+
# Each line keeps its sender (Steer.source_for_client): a chi send or
|
|
576
|
+
# a delegating parent's line is saved and shown to the model as theirs.
|
|
577
|
+
merged.map { |prompt, origin| Steer::Line.new(text: prompt, source: Steer.source_for_client(origin&.dig(:client_id))) }
|
|
575
578
|
end
|
|
576
579
|
end
|
|
577
580
|
|
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.24.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- lib/samagotchi/engine.rb
|
|
224
224
|
- lib/samagotchi/events.rb
|
|
225
225
|
- lib/samagotchi/gem_update.rb
|
|
226
|
+
- lib/samagotchi/generation_phase.rb
|
|
226
227
|
- lib/samagotchi/guardrail_wiring.rb
|
|
227
228
|
- lib/samagotchi/guardrails.rb
|
|
228
229
|
- lib/samagotchi/guardrails/approval.rb
|
|
@@ -380,6 +381,7 @@ files:
|
|
|
380
381
|
- lib/samagotchi/tool_declarations.rb
|
|
381
382
|
- lib/samagotchi/tool_response.rb
|
|
382
383
|
- lib/samagotchi/tool_runner.rb
|
|
384
|
+
- lib/samagotchi/tool_view.rb
|
|
383
385
|
- lib/samagotchi/tools/args.rb
|
|
384
386
|
- lib/samagotchi/tools/ask_user_question.rb
|
|
385
387
|
- lib/samagotchi/tools/builtin_calls.rb
|
|
@@ -420,6 +422,7 @@ files:
|
|
|
420
422
|
- lib/samagotchi/version.rb
|
|
421
423
|
- lib/samagotchi/vision_context.rb
|
|
422
424
|
- lib/samagotchi/vision_support.rb
|
|
425
|
+
- lib/samagotchi/waiting_steer.rb
|
|
423
426
|
- lib/samagotchi/web/app.rb
|
|
424
427
|
- lib/samagotchi/web/info_probe.rb
|
|
425
428
|
- lib/samagotchi/web/lan.rb
|
|
@@ -434,6 +437,7 @@ files:
|
|
|
434
437
|
- lib/samagotchi/web/public/chunk_router.js
|
|
435
438
|
- lib/samagotchi/web/public/command_complete.js
|
|
436
439
|
- lib/samagotchi/web/public/command_list.js
|
|
440
|
+
- lib/samagotchi/web/public/command_view.js
|
|
437
441
|
- lib/samagotchi/web/public/composer_size.js
|
|
438
442
|
- lib/samagotchi/web/public/copy.js
|
|
439
443
|
- lib/samagotchi/web/public/ctx.js
|