samagotchi 0.5.0 → 0.6.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 +84 -1
- data/bin/chi +32 -891
- data/docs/architecture.md +8 -1
- data/docs/cli.md +12 -13
- data/docs/configuration.md +4 -1
- data/docs/desktop.md +3 -1
- data/docs/hooks.md +69 -4
- data/docs/plugins.md +98 -3
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bridge/card_store.rb +30 -5
- data/lib/samagotchi/bridge/turn_accumulator.rb +7 -4
- data/lib/samagotchi/bridge/turn_notice.rb +37 -0
- data/lib/samagotchi/bundle_command.rb +652 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +4 -4
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +214 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -0
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/cancellation_controller.rb +42 -1
- data/lib/samagotchi/config.rb +2 -2
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +20 -5
- data/lib/samagotchi/desktop/macos/Panel.swift +31 -3
- data/lib/samagotchi/engine.rb +138 -618
- data/lib/samagotchi/guardrail_wiring.rb +238 -0
- data/lib/samagotchi/hooks/registry.rb +11 -3
- data/lib/samagotchi/hooks/stream_watch.rb +93 -0
- data/lib/samagotchi/hooks.rb +3 -0
- data/lib/samagotchi/kernel_loop.rb +65 -6
- data/lib/samagotchi/llm/chat_loop.rb +73 -11
- data/lib/samagotchi/llm/openai_chat.rb +16 -4
- data/lib/samagotchi/log_subscriber.rb +10 -4
- data/lib/samagotchi/plugin/api.rb +8 -0
- data/lib/samagotchi/plugin/context.rb +12 -3
- data/lib/samagotchi/plugin_tasks.rb +229 -0
- data/lib/samagotchi/question_desk.rb +280 -0
- data/lib/samagotchi/recap_setup.rb +128 -0
- data/lib/samagotchi/reply_wait.rb +9 -0
- data/lib/samagotchi/send_command.rb +3 -6
- data/lib/samagotchi/session.rb +9 -1
- data/lib/samagotchi/sessions_command.rb +255 -0
- data/lib/samagotchi/steer.rb +19 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +18 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -2
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +6 -2
- data/lib/samagotchi/tools/delegate_wait.rb +26 -8
- data/lib/samagotchi/turn_note.rb +20 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +28 -10
- data/lib/samagotchi/web/public/app.js +94 -52
- data/lib/samagotchi/web/public/card.js +8 -1
- data/lib/samagotchi/web/public/format.js +1 -1
- data/lib/samagotchi/web/public/index.html +2 -5
- data/lib/samagotchi/web/public/stage_model.js +44 -0
- data/lib/samagotchi/web/public/stage_view.js +26 -17
- data/lib/samagotchi/web/public/timing.js +9 -3
- data/lib/samagotchi/web/public/turn_events.js +18 -5
- data/lib/samagotchi/web/public/turn_view.js +80 -16
- data/lib/samagotchi/web/server.rb +4 -3
- metadata +9 -2
- data/lib/samagotchi/web/public/chat_view.js +0 -376
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
// reload. Slots are patched in place, never rebuilt from HTML: a selection
|
|
12
12
|
// and annotate survive. The rules are pure in stage_model.js.
|
|
13
13
|
import { createTurnView } from "./turn_view.js";
|
|
14
|
-
import { handOffDue, handOffOrder, inUseFrom, isPlain, liveSlots, placeFor } from "./stage_model.js";
|
|
15
|
-
import { splitSentences } from "./sentences.js";
|
|
14
|
+
import { handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor } from "./stage_model.js";
|
|
16
15
|
import { formatDuration } from "./timing.js";
|
|
17
16
|
|
|
18
17
|
const COLLAPSE_KEY = "chi_stage_collapsed";
|
|
@@ -23,6 +22,10 @@ const SETTLE_CAP_MS = 8000;
|
|
|
23
22
|
const FOLD_MS = 420;
|
|
24
23
|
const FLASH_MS = 4000;
|
|
25
24
|
const PENDING = ".bubble.question:not(.answered):not(.cancelled), .bubble.continue:not(.resolved)";
|
|
25
|
+
// A running turn's card with actions (check-in's Nudge / Keep going / Stop)
|
|
26
|
+
// asks the user too, but the turn goes on: it waits only while the turn runs
|
|
27
|
+
// and never holds the hand-off (a turn cut short can leave it open).
|
|
28
|
+
const ASKS = ".plugin-card[data-in-turn] .card-action";
|
|
26
29
|
const MARKS = { answered: "✓", canceled: "■", failed: "✕" };
|
|
27
30
|
|
|
28
31
|
function node(tag, cls, parent = null) {
|
|
@@ -32,16 +35,6 @@ function node(tag, cls, parent = null) {
|
|
|
32
35
|
return el;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
// The step's newest narration sentence: the end of a finished text is a
|
|
36
|
-
// boundary (its last line may have no period), a live one's not yet.
|
|
37
|
-
function headlineOf(text, done) {
|
|
38
|
-
const { sentences, rest } = splitSentences(text, { atEnd: true });
|
|
39
|
-
const fragment = rest.replace(/\s+/g, " ").trim();
|
|
40
|
-
if (done && fragment) return fragment;
|
|
41
|
-
if (sentences.length) return sentences[sentences.length - 1];
|
|
42
|
-
return fragment.length > 160 ? fragment : "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
function readCollapsed() {
|
|
46
39
|
try { return localStorage.getItem(COLLAPSE_KEY) === "1"; } catch (_) { return false; }
|
|
47
40
|
}
|
|
@@ -153,6 +146,13 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
153
146
|
return !!(extras.querySelector(PENDING) || tail.querySelector(PENDING));
|
|
154
147
|
}
|
|
155
148
|
|
|
149
|
+
function asksCard() {
|
|
150
|
+
return !endKind && !!extras.querySelector(ASKS);
|
|
151
|
+
}
|
|
152
|
+
// A card answered or redrawn in place (its buttons gone) changes the
|
|
153
|
+
// status without a turn event.
|
|
154
|
+
new MutationObserver(() => scheduleRefresh()).observe(extras, { childList: true, subtree: true });
|
|
155
|
+
|
|
156
156
|
function refresh() {
|
|
157
157
|
rafPending = false;
|
|
158
158
|
const { turn, blockEl } = view.current();
|
|
@@ -169,12 +169,14 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
169
169
|
if (blockEl && blockEl.parentNode === cloudBody) blockEl.open = true;
|
|
170
170
|
const narration = gen?.text ? headlineOf(gen.text, gen.textDone || gen.closed) : "";
|
|
171
171
|
const thinking = thinkingLine && thinkingLine.step === turn.gens.length ? thinkingLine.text : "";
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
// A question blocks the turn: its card takes the live slots' room. An
|
|
173
|
+
// asking card's turn goes on: the status waits, the slots stay.
|
|
174
|
+
const blocked = pendingCard();
|
|
175
|
+
const slots = liveSlots(turn, { narration, thinking, pending: blocked || asksCard(), ended: endKind });
|
|
174
176
|
|
|
175
177
|
stage.classList.toggle("plain", plain);
|
|
176
178
|
stage.classList.toggle("ended", !!endKind);
|
|
177
|
-
stage.classList.toggle("waiting",
|
|
179
|
+
stage.classList.toggle("waiting", blocked);
|
|
178
180
|
stage.dataset.phase = slots.phase;
|
|
179
181
|
mark.textContent = MARKS[slots.phase] || "";
|
|
180
182
|
phaseEl.textContent = slots.phase;
|
|
@@ -521,6 +523,11 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
521
523
|
turnEnded(args) {
|
|
522
524
|
const kept = view.turnEnded(args) || [];
|
|
523
525
|
if (!holding) return kept;
|
|
526
|
+
// The warn cards the extras kept in sight leave the turn with the
|
|
527
|
+
// block's (card.js leavesBlock): after the end line, as a reload has them.
|
|
528
|
+
const warn = [...extras.querySelectorAll(":scope > .plugin-card.warn")];
|
|
529
|
+
warn.forEach((el) => el.remove());
|
|
530
|
+
kept.push(...warn);
|
|
524
531
|
view.chronological();
|
|
525
532
|
endKind = args.kind;
|
|
526
533
|
endedAt = Date.now();
|
|
@@ -542,7 +549,9 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
542
549
|
return placed;
|
|
543
550
|
},
|
|
544
551
|
steer: (data) => view.steer(data),
|
|
545
|
-
card
|
|
552
|
+
// A warn card (a plugin's warning) stays in sight: false puts it in the
|
|
553
|
+
// extras (place), not in its step inside the closed cloud.
|
|
554
|
+
card: (el) => !el.classList.contains("warn") && view.card(el),
|
|
546
555
|
flush: () => view.flush(),
|
|
547
556
|
answerReady() {
|
|
548
557
|
settled = true;
|
|
@@ -564,7 +573,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
564
573
|
const stick = slot.scrollHeight - slot.scrollTop - slot.clientHeight < 24;
|
|
565
574
|
slot.appendChild(el);
|
|
566
575
|
slot.hidden = false;
|
|
567
|
-
if (stick || el.matches?.(PENDING)) slot.scrollTop = slot.scrollHeight;
|
|
576
|
+
if (stick || el.matches?.(PENDING) || el.querySelector?.(ASKS)) slot.scrollTop = slot.scrollHeight;
|
|
568
577
|
scheduleRefresh();
|
|
569
578
|
return true;
|
|
570
579
|
},
|
|
@@ -88,7 +88,9 @@ export function timedTurnIndexes(items) {
|
|
|
88
88
|
// the last, in order; a text-less iteration leaves no message, so a text
|
|
89
89
|
// can sit one step early: inspection, not truth) and its answer (the last
|
|
90
90
|
// assistant message). Assistant messages before any prompt are answers on
|
|
91
|
-
// their own. The turn index counts prompts, as timedTurnIndexes does.
|
|
91
|
+
// their own. The turn index counts prompts, as timedTurnIndexes does. The
|
|
92
|
+
// user's lines merged into the running turn (`merged`, the server's
|
|
93
|
+
// Steer::INPUT_KIND) stay in it: the turn's `merged` list.
|
|
92
94
|
export function turnGroups(items, timing) {
|
|
93
95
|
const groups = [];
|
|
94
96
|
let turnIndex = -1;
|
|
@@ -99,9 +101,11 @@ export function turnGroups(items, timing) {
|
|
|
99
101
|
} else if (item?.role === "steer") {
|
|
100
102
|
if (turn) turn.steers.push(i);
|
|
101
103
|
else groups.push({ kind: "steer", i });
|
|
104
|
+
} else if (item?.role === "user" && item.merged && turn) {
|
|
105
|
+
turn.merged.push(i);
|
|
102
106
|
} else if (item?.role === "user") {
|
|
103
107
|
turnIndex += 1;
|
|
104
|
-
turn = { kind: "turn", turnIndex, user: i, texts: [], steers: [] };
|
|
108
|
+
turn = { kind: "turn", turnIndex, user: i, texts: [], steers: [], merged: [] };
|
|
105
109
|
groups.push(turn);
|
|
106
110
|
} else if (item?.role === "assistant") {
|
|
107
111
|
if (turn) turn.texts.push(i);
|
|
@@ -110,7 +114,9 @@ export function turnGroups(items, timing) {
|
|
|
110
114
|
});
|
|
111
115
|
return groups.map((g) => {
|
|
112
116
|
if (g.kind !== "turn") return g;
|
|
113
|
-
|
|
117
|
+
const group = withSteers(turnGroup(g, items, timing), g.steers, items);
|
|
118
|
+
// A turn with no merged lines keeps its shape.
|
|
119
|
+
return g.merged.length ? { ...group, merged: g.merged } : group;
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
122
|
|
|
@@ -55,9 +55,18 @@ export function hookNoticeLabel(hook) {
|
|
|
55
55
|
return /^[a-z][a-z0-9-]*$/.test(name) ? name : "hook";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// The row a step gets when the loop asks again after an empty answer
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
// The row a step gets when the loop asks again after an empty answer, or
|
|
59
|
+
// after a plugin cut the generation (stopped_by: the bundle).
|
|
60
|
+
export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy } = {}) {
|
|
61
|
+
const what = stoppedBy ? `cut by ${stoppedBy}` : "empty answer";
|
|
62
|
+
return `↻ ${what}, asking again (${attempt}/${of})`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The text of a turn's row from a snapshot entry (CardStore): a hook's
|
|
66
|
+
// notice as "<label>: text", the loop's asking-again row as its line.
|
|
67
|
+
export function noticeLine(entry = {}) {
|
|
68
|
+
if (entry.type === "empty_answer_retry") return emptyRetryLine(entry);
|
|
69
|
+
return `${hookNoticeLabel(entry.hook)}: ${entry.text || ""}`;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
// The live turn's status while the provider is asked again after an error
|
|
@@ -211,6 +220,7 @@ export function promptOps(event, { myId, known, unmatchedMerge = false }) {
|
|
|
211
220
|
// parts in order), then the prompts queued behind it. app.js feeds these to
|
|
212
221
|
// its stream handlers, so a join renders exactly like watching live.
|
|
213
222
|
// `merged_input` is the one synthetic type: a prompt merged into the turn.
|
|
223
|
+
// A `notice` part carries its event as it came (TurnNotice).
|
|
214
224
|
export function snapshotEvents({ current_turn: turn = null, queued = [], started_at = undefined } = {}) {
|
|
215
225
|
const events = [];
|
|
216
226
|
if (turn) {
|
|
@@ -256,9 +266,12 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
256
266
|
closeText();
|
|
257
267
|
events.push({ type: "reminder_injected", reminders: part.reminders || [] });
|
|
258
268
|
break;
|
|
259
|
-
|
|
269
|
+
// One of the turn's rows (a hook's notice, an empty-answer retry, a
|
|
270
|
+
// question and its answer): the event itself, which the live
|
|
271
|
+
// handler draws where it drew it live.
|
|
272
|
+
case "notice":
|
|
260
273
|
closeText();
|
|
261
|
-
events.push({
|
|
274
|
+
if (part.event?.type) events.push({ ...part.event });
|
|
262
275
|
break;
|
|
263
276
|
// A plugin's steer: a steer-only merge, as live.
|
|
264
277
|
case "steer":
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
// The turn view of a running turn (web.
|
|
2
|
-
// class="turn-work"> per turn holding one <details
|
|
3
|
-
// generation, the current one live (open, at the bottom),
|
|
4
|
-
// collapsed to one line each. At the turn's end the last
|
|
5
|
-
// narration moves out of the block as the answer bubble, so
|
|
6
|
-
// annotate and the turn output dedupe work
|
|
7
|
-
//
|
|
1
|
+
// The turn view of a running turn (web.view: turn, and the stage's block):
|
|
2
|
+
// one <details class="turn-work"> per turn holding one <details
|
|
3
|
+
// class="gen"> per generation, the current one live (open, at the bottom),
|
|
4
|
+
// the earlier ones collapsed to one line each. At the turn's end the last
|
|
5
|
+
// generation's narration moves out of the block as the answer bubble, so
|
|
6
|
+
// markdown, annotate and the turn output dedupe work on a plain bubble.
|
|
7
|
+
// The grouping rules live in turn_model.js.
|
|
8
8
|
//
|
|
9
9
|
// A block that holds one gen with no thinking and no tool rows hides its
|
|
10
10
|
// chrome (CSS :has), so a plain answer streams like today's bubble, and is
|
|
11
11
|
// removed at the end: the answer bubble takes its place.
|
|
12
12
|
import { toolName } from "./activity.js";
|
|
13
13
|
import { leavesBlock } from "./card.js";
|
|
14
|
-
import { activityStatusClass, activityStatusLabel, fillActivityRow } from "./chat_view.js";
|
|
15
14
|
import { copySourceAttr } from "./copy.js";
|
|
16
15
|
import { activityDiffHtml } from "./diff_view.js";
|
|
17
16
|
import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
|
|
17
|
+
import { thumbsHtml } from "./images.js";
|
|
18
18
|
import { shouldFollowScroll } from "./scroll.js";
|
|
19
19
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
20
20
|
import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
|
|
@@ -23,6 +23,55 @@ import { createTicker } from "./thinking_ticker.js";
|
|
|
23
23
|
import * as model from "./turn_model.js";
|
|
24
24
|
import { createHold } from "./hold.js";
|
|
25
25
|
|
|
26
|
+
// A row's status (live, or a saved tool record's on reload): running,
|
|
27
|
+
// error, stopped (a task_wait the user's Stop ended), else ok ("done").
|
|
28
|
+
function activityStatusClass(status) {
|
|
29
|
+
if (status === "running") return "running";
|
|
30
|
+
if (status === "error") return "error";
|
|
31
|
+
if (status === "stopped") return "stopped";
|
|
32
|
+
return "ok";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function activityStatusLabel(status) {
|
|
36
|
+
if (status === "running") return "running";
|
|
37
|
+
if (status === "error") return "error";
|
|
38
|
+
if (status === "stopped") return "stopped";
|
|
39
|
+
return "done";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A live tool row filled from its activity row (status, title, output).
|
|
43
|
+
function fillActivityRow(el, row, sessionId) {
|
|
44
|
+
const statusEl = el.querySelector(".activity-status");
|
|
45
|
+
statusEl.className = `activity-status ${activityStatusClass(row.status)}`;
|
|
46
|
+
statusEl.textContent = activityStatusLabel(row.status);
|
|
47
|
+
const toolEl = el.querySelector(".activity-tool");
|
|
48
|
+
if (toolEl) toolEl.textContent = toolName(row);
|
|
49
|
+
const paramsEl = el.querySelector(".activity-params");
|
|
50
|
+
if (paramsEl) {
|
|
51
|
+
// The title in place of the params; the full params line on hover.
|
|
52
|
+
paramsEl.textContent = row.title || row.params || "";
|
|
53
|
+
paramsEl.title = row.title && row.params ? row.params : "";
|
|
54
|
+
}
|
|
55
|
+
const outEl = el.querySelector(".activity-output");
|
|
56
|
+
if (outEl) {
|
|
57
|
+
if (row.status === "running") {
|
|
58
|
+
outEl.style.display = "none";
|
|
59
|
+
outEl.textContent = "";
|
|
60
|
+
outEl.title = "";
|
|
61
|
+
} else {
|
|
62
|
+
// dispatch prefixes output with "[tool]"; strip that noise for display.
|
|
63
|
+
const full = (row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
64
|
+
outEl.title = full;
|
|
65
|
+
outEl.textContent = full.length > 300 ? `${full.slice(0, 300)}…` : full;
|
|
66
|
+
outEl.style.display = full ? "" : "none";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// The images a tool returned (a reloaded row has them too, reloadRowHtml).
|
|
70
|
+
if (row.images?.length && !el.querySelector(".thumbs")) el.insertAdjacentHTML("beforeend", thumbsHtml(sessionId, row.images));
|
|
71
|
+
// An edit/write's change, collapsed: "diff +3 −1".
|
|
72
|
+
if (row.diff && !el.querySelector(".activity-diff")) el.insertAdjacentHTML("beforeend", activityDiffHtml(row.diff));
|
|
73
|
+
}
|
|
74
|
+
|
|
26
75
|
// The container seams (the stage view composes this view, stage_view.js):
|
|
27
76
|
// blockHost(el) puts the turn's block in the page (default: the history);
|
|
28
77
|
// follow() keeps the live part in view (default: the history's
|
|
@@ -102,7 +151,7 @@ export function createTurnView({
|
|
|
102
151
|
const text = document.createElement("div");
|
|
103
152
|
text.className = "gen-text streaming";
|
|
104
153
|
// A step after the first shows "…" until its text comes (the first one
|
|
105
|
-
// only for a prompt
|
|
154
|
+
// only for a prompt).
|
|
106
155
|
if (turn.gens.length > 1) text.textContent = "…";
|
|
107
156
|
el.appendChild(text);
|
|
108
157
|
const entry = { el, summary, thinking: null, text, activity: null, rows: new Map(), toggled: false,
|
|
@@ -308,13 +357,25 @@ export function createTurnView({
|
|
|
308
357
|
}
|
|
309
358
|
entry.summary.textContent = model.genLabel(gen);
|
|
310
359
|
if (!entry.toggled) entry.el.open = false;
|
|
311
|
-
//
|
|
360
|
+
// Every generation's text goes into seenContent (the turn output dedupe).
|
|
312
361
|
const k = normalize(gen.text);
|
|
313
362
|
if (k) sawText(k);
|
|
314
363
|
}
|
|
315
364
|
|
|
365
|
+
// A step that closes mid-turn (the next one starts) collapses: a warn card
|
|
366
|
+
// in it would go out of sight until the turn ends. It moves out of the
|
|
367
|
+
// step, right after it in the block (open while the turn runs), where
|
|
368
|
+
// turnEnded still finds it and takes it out of the block (leavesBlock).
|
|
369
|
+
function liftWarnCards(gen) {
|
|
370
|
+
const entry = genEls.get(gen);
|
|
371
|
+
if (!entry) return;
|
|
372
|
+
const cards = [...entry.el.querySelectorAll(".plugin-card.warn")];
|
|
373
|
+
if (cards.length) entry.el.after(...cards);
|
|
374
|
+
}
|
|
375
|
+
|
|
316
376
|
function apply(r) {
|
|
317
377
|
if (!r) return;
|
|
378
|
+
if (r.closed && r.opened) liftWarnCards(r.closed);
|
|
318
379
|
if (r.closed) closeGenEl(r.closed);
|
|
319
380
|
if (r.opened) createGenEl(r.gen);
|
|
320
381
|
if (r.closed || r.opened) updateBlockSummary();
|
|
@@ -437,8 +498,8 @@ export function createTurnView({
|
|
|
437
498
|
cards.forEach((el) => { if (!el.classList.contains("plugin-card")) el.classList.add("bubble"); });
|
|
438
499
|
if (leaving) entry.el.remove();
|
|
439
500
|
if (plain) {
|
|
440
|
-
// A plain answer: no block, the bubble where it was
|
|
441
|
-
//
|
|
501
|
+
// A plain answer: no block, the bubble where it was; its thinking, if
|
|
502
|
+
// any, as a collapsed thinking block above it.
|
|
442
503
|
const pieces = [];
|
|
443
504
|
if (gen.thinking) {
|
|
444
505
|
if (!entry.thinkingWrap) thinkingEl(gen);
|
|
@@ -473,7 +534,7 @@ export function createTurnView({
|
|
|
473
534
|
const last = model.currentGen(turn);
|
|
474
535
|
// A completed turn's answer is its last generation's text; a one-step
|
|
475
536
|
// turn without tools leaves the block in any case (canceled, it is the
|
|
476
|
-
// partial text bubble
|
|
537
|
+
// partial text bubble).
|
|
477
538
|
const promoting = last && (kind === "completed" || (turn.gens.length === 1 && last.tools.length === 0));
|
|
478
539
|
// The box's height, measured while it is still in the DOM and the step
|
|
479
540
|
// still open: the apply below swaps it for the full text and collapses
|
|
@@ -601,8 +662,7 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
601
662
|
};
|
|
602
663
|
}
|
|
603
664
|
|
|
604
|
-
// A reloaded tool row, the live row's markup (
|
|
605
|
-
// fillActivityRow): status, tool, params, output (its "[tool]" tag dropped,
|
|
665
|
+
// A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params, output (its "[tool]" tag dropped,
|
|
606
666
|
// 300 chars, the rest on hover), plus the call's duration from its record.
|
|
607
667
|
// Params, output and the call's images come from the saved messages'
|
|
608
668
|
// parts; without them the row has the record's name, status and duration
|
|
@@ -651,7 +711,7 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
651
711
|
const noBlock = !group.steps.length ||
|
|
652
712
|
(group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking);
|
|
653
713
|
if (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking) {
|
|
654
|
-
// A plain answer with thinking: the
|
|
714
|
+
// A plain answer with thinking: the collapsed thinking block, as live.
|
|
655
715
|
parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
|
|
656
716
|
} else if (group.steps.length) {
|
|
657
717
|
const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
|
|
@@ -672,6 +732,10 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
672
732
|
// their own after the prompt.
|
|
673
733
|
const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
|
|
674
734
|
parts.push(loose.map((i) => steerHtml(i, true)).join(""));
|
|
735
|
+
// The user's lines merged into the turn: the live "steered" bubbles,
|
|
736
|
+
// after its steps and before its answer.
|
|
737
|
+
parts.push((group.merged || []).map((i) =>
|
|
738
|
+
`<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}><div class="user-message">${messageBodyHtml(items[i]).body}${thumbs(items[i].images)}</div><span class="state-badge">steered</span></div>`).join(""));
|
|
675
739
|
// A turn with no answer (canceled, or running: no record yet) shows its
|
|
676
740
|
// timing after its prompt and steps, where the live view leaves it.
|
|
677
741
|
if (group.answer === null) parts.push(timingHtml(group));
|
|
@@ -59,8 +59,9 @@ module Samagotchi
|
|
|
59
59
|
# @param new_token [Boolean] replace the LAN access token first
|
|
60
60
|
# (--new-token): a running server takes the new one at once
|
|
61
61
|
# @return [Integer] the exit status
|
|
62
|
-
def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false,
|
|
63
|
-
|
|
62
|
+
def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false,
|
|
63
|
+
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
|
|
64
|
+
new_token: false)
|
|
64
65
|
rotate_token if new_token
|
|
65
66
|
setting = host_setting(host)
|
|
66
67
|
lan = Lan.wanted?(setting) ? Lan.choose(setting) : nil
|
|
@@ -108,7 +109,7 @@ module Samagotchi
|
|
|
108
109
|
# @param token_path [String] the LAN access token's file
|
|
109
110
|
# @return [Boolean] false when the port was taken (said so on stderr)
|
|
110
111
|
def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
|
|
111
|
-
view: "
|
|
112
|
+
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
|
|
112
113
|
token_path: Token.path)
|
|
113
114
|
port = resolve_port(port)
|
|
114
115
|
host = resolve_host(host)
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -150,9 +150,11 @@ files:
|
|
|
150
150
|
- lib/samagotchi/bridge/ring_buffer.rb
|
|
151
151
|
- lib/samagotchi/bridge/sse_writer.rb
|
|
152
152
|
- lib/samagotchi/bridge/turn_accumulator.rb
|
|
153
|
+
- lib/samagotchi/bridge/turn_notice.rb
|
|
153
154
|
- lib/samagotchi/bridge_client.rb
|
|
154
155
|
- lib/samagotchi/bridge_client/event_stream.rb
|
|
155
156
|
- lib/samagotchi/bridge_client/sse_parser.rb
|
|
157
|
+
- lib/samagotchi/bundle_command.rb
|
|
156
158
|
- lib/samagotchi/bundle_needs.rb
|
|
157
159
|
- lib/samagotchi/bundles/btw/manifest.yml
|
|
158
160
|
- lib/samagotchi/bundles/btw/plugin.rb
|
|
@@ -201,6 +203,7 @@ files:
|
|
|
201
203
|
- lib/samagotchi/empty_answer_retry.rb
|
|
202
204
|
- lib/samagotchi/engine.rb
|
|
203
205
|
- lib/samagotchi/gem_update.rb
|
|
206
|
+
- lib/samagotchi/guardrail_wiring.rb
|
|
204
207
|
- lib/samagotchi/guardrails.rb
|
|
205
208
|
- lib/samagotchi/guardrails/approval.rb
|
|
206
209
|
- lib/samagotchi/guardrails/approvals.rb
|
|
@@ -216,6 +219,7 @@ files:
|
|
|
216
219
|
- lib/samagotchi/hooks/bundle_loader.rb
|
|
217
220
|
- lib/samagotchi/hooks/loader.rb
|
|
218
221
|
- lib/samagotchi/hooks/registry.rb
|
|
222
|
+
- lib/samagotchi/hooks/stream_watch.rb
|
|
219
223
|
- lib/samagotchi/host_registry.rb
|
|
220
224
|
- lib/samagotchi/idle_client.rb
|
|
221
225
|
- lib/samagotchi/idle_recap.rb
|
|
@@ -269,9 +273,12 @@ files:
|
|
|
269
273
|
- lib/samagotchi/plugin/sessions.rb
|
|
270
274
|
- lib/samagotchi/plugin/side_question.rb
|
|
271
275
|
- lib/samagotchi/plugin/tool_result.rb
|
|
276
|
+
- lib/samagotchi/plugin_tasks.rb
|
|
272
277
|
- lib/samagotchi/project_scope.rb
|
|
273
278
|
- lib/samagotchi/prompt.rb
|
|
274
279
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
280
|
+
- lib/samagotchi/question_desk.rb
|
|
281
|
+
- lib/samagotchi/recap_setup.rb
|
|
275
282
|
- lib/samagotchi/recap_store.rb
|
|
276
283
|
- lib/samagotchi/reminder_store.rb
|
|
277
284
|
- lib/samagotchi/reply_wait.rb
|
|
@@ -286,6 +293,7 @@ files:
|
|
|
286
293
|
- lib/samagotchi/session_manager.rb
|
|
287
294
|
- lib/samagotchi/session_metrics.rb
|
|
288
295
|
- lib/samagotchi/session_observer.rb
|
|
296
|
+
- lib/samagotchi/sessions_command.rb
|
|
289
297
|
- lib/samagotchi/steer.rb
|
|
290
298
|
- lib/samagotchi/system_prompt.rb
|
|
291
299
|
- lib/samagotchi/terminal_ui.rb
|
|
@@ -360,7 +368,6 @@ files:
|
|
|
360
368
|
- lib/samagotchi/web/public/annotations.js
|
|
361
369
|
- lib/samagotchi/web/public/app.js
|
|
362
370
|
- lib/samagotchi/web/public/card.js
|
|
363
|
-
- lib/samagotchi/web/public/chat_view.js
|
|
364
371
|
- lib/samagotchi/web/public/chunk_router.js
|
|
365
372
|
- lib/samagotchi/web/public/command_complete.js
|
|
366
373
|
- lib/samagotchi/web/public/composer_size.js
|