samagotchi 0.66.0 → 0.68.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 +55 -30
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +129 -38
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -1
- data/docs/testing.md +18 -4
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +307 -549
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +21 -11
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +71 -93
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +1 -1
- data/lib/samagotchi/web/public/app.js +103 -539
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/commands.js +2 -2
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +15 -7
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +2 -8
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/session_state.js +14 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +35 -2
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +22 -24
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +27 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The stage view's cap and frame (stage_view.js): the one place that
|
|
2
|
+
// writes the stage's height, and the stage's own scrolls. The cap: at most
|
|
3
|
+
// ~60% of the page (~80% with the cloud open), and never above the session
|
|
4
|
+
// panel. The frame: held and grow-only while the stage holds a turn
|
|
5
|
+
// (stage_model.js frameTarget), its growth eased. The stage's state comes
|
|
6
|
+
// in as getters (+isHolding+, +isCollapsed+, +isCloudOpen+); what the stage
|
|
7
|
+
// changes of the frame's goes through resetHeld and forgetContent.
|
|
8
|
+
import { frameTarget } from "./stage_model.js";
|
|
9
|
+
import { easeScrollTo } from "./scroll.js";
|
|
10
|
+
|
|
11
|
+
// The stage grows (a card, the long answer, the cloud opening) with a short
|
|
12
|
+
// ease: deliberate, never a snap.
|
|
13
|
+
const GROW_MS = 180;
|
|
14
|
+
const GROW_EASE = "cubic-bezier(.2,.7,.3,1)";
|
|
15
|
+
// The room between the stage and the composer card (index.html's margin-bottom).
|
|
16
|
+
const STAGE_GAP = 10;
|
|
17
|
+
|
|
18
|
+
export function reducedMotion() {
|
|
19
|
+
return matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// +observed+: the elements whose size changes refit the frame (the stage's
|
|
23
|
+
// slots, in the order they are observed).
|
|
24
|
+
export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, holdEl, asking, extras, dockEl, centerEl, composerEl, observed, isHolding, isCollapsed, isCloudOpen }) {
|
|
25
|
+
// The frame (fitFrame): the height held while the turn runs and the
|
|
26
|
+
// tallest content seen.
|
|
27
|
+
let held = null;
|
|
28
|
+
let maxContent = 0;
|
|
29
|
+
let growth = null; // the running ease of the frame's growth (an Animation)
|
|
30
|
+
let growthTo = 0; // its end height
|
|
31
|
+
|
|
32
|
+
// The stage's own scrolls, eased (instant with reduced motion).
|
|
33
|
+
function easeStage(top) {
|
|
34
|
+
if (Math.abs(top - scroller.scrollTop) < 1) return;
|
|
35
|
+
easeScrollTo(scroller, top, { reduce: reducedMotion() });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// +fn+ once the frame has grown for a change made now: two frames on (the
|
|
39
|
+
// ResizeObserver starts the ease in the frame after the change), then at
|
|
40
|
+
// the ease's end.
|
|
41
|
+
function afterGrowth(fn) {
|
|
42
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
43
|
+
if (growth?.playState === "running") growth.finished.then(fn, fn);
|
|
44
|
+
else fn();
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── the cap: at most ~60% of the page (~80% with the cloud open), and
|
|
49
|
+
// never above the session panel (the dock is absolute: a tall composer or
|
|
50
|
+
// a phone's keyboard would push the stage's top out) ────────────────────
|
|
51
|
+
|
|
52
|
+
// Where the stage's scroll goes so +slot+ (the scroller's child) shows:
|
|
53
|
+
// its end in sight, and the asking text's top too when it and the slot
|
|
54
|
+
// (a waiting card) fit together (the reply reads from its start, the card
|
|
55
|
+
// still in sight). null: nothing to show.
|
|
56
|
+
function revealTop(slot) {
|
|
57
|
+
if (stage.hidden || slot.hidden) return null;
|
|
58
|
+
let top = Math.max(scroller.scrollTop, slot.offsetTop + slot.offsetHeight - scroller.clientHeight);
|
|
59
|
+
if (!asking.hidden && asking.offsetTop < top && slot.offsetTop + slot.offsetHeight - asking.offsetTop <= scroller.clientHeight) top = asking.offsetTop;
|
|
60
|
+
return top;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Scrolls the stage to show +slot+: eased, or at once (+now+: the cap
|
|
64
|
+
// dropped under it).
|
|
65
|
+
function reveal(slot, { now = false } = {}) {
|
|
66
|
+
const top = revealTop(slot);
|
|
67
|
+
if (top === null) return;
|
|
68
|
+
if (now) scroller.scrollTop = top;
|
|
69
|
+
else easeStage(top);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fitCap() {
|
|
73
|
+
if (stage.hidden) return;
|
|
74
|
+
// The composer card and the gap above it (the stage floats over them,
|
|
75
|
+
// out of the dock's flow).
|
|
76
|
+
const rest = dockEl.offsetHeight + STAGE_GAP;
|
|
77
|
+
const room = centerEl.clientHeight - rest - 32;
|
|
78
|
+
const share = window.innerHeight * (isCloudOpen() ? 0.8 : 0.6);
|
|
79
|
+
const cap = Math.max(96, Math.round(Math.min(share, room)));
|
|
80
|
+
const was = capPx();
|
|
81
|
+
if (cap === was) return;
|
|
82
|
+
stage.style.setProperty("--stage-max", `${cap}px`);
|
|
83
|
+
fitFrame();
|
|
84
|
+
// A smaller cap (a phone's keyboard came up) keeps a waiting card in
|
|
85
|
+
// sight: the asking text above it would push it out. A larger one
|
|
86
|
+
// leaves the reader's scroll alone.
|
|
87
|
+
if (cap < was && stage.classList.contains("waiting")) {
|
|
88
|
+
requestAnimationFrame(() => reveal(extras, { now: true }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
new ResizeObserver(() => fitCap()).observe(centerEl);
|
|
92
|
+
new ResizeObserver(() => fitCap()).observe(composerEl);
|
|
93
|
+
window.addEventListener("resize", fitCap);
|
|
94
|
+
|
|
95
|
+
// ── the frame: the one place that writes the stage's height ────────────
|
|
96
|
+
// Nothing in the stage view jumps. The rules: the stage is out of the
|
|
97
|
+
// dock's flow (index.html), so its height never moves the composer; while
|
|
98
|
+
// it holds a turn its height is held and grow-only (frameTarget), so a
|
|
99
|
+
// content change happens inside it; growth eases (frameTo); the chip is a
|
|
100
|
+
// footer no content can push; the hand-off releases the turn at once
|
|
101
|
+
// (releaseTurn) and empties the stage only after it slid away under the
|
|
102
|
+
// composer (a transform, then finishFold/clearStage).
|
|
103
|
+
|
|
104
|
+
// The stage's height, +px+. +animate+: a growth eases (GROW_MS, Web
|
|
105
|
+
// Animations: the running ease is +growth+), unless motion is reduced or
|
|
106
|
+
// the stage is hidden or collapsed; a shrink (the cap dropped) is at once.
|
|
107
|
+
function frameTo(px, { animate = false } = {}) {
|
|
108
|
+
const from = stage.offsetHeight;
|
|
109
|
+
stage.style.height = `${px}px`;
|
|
110
|
+
if (px === from) return;
|
|
111
|
+
const ease = animate && px > from && !isCollapsed() && !stage.hidden && !document.hidden && !reducedMotion();
|
|
112
|
+
if (growth && (!ease || growth.playState !== "running" || growthTo !== px)) {
|
|
113
|
+
growth.cancel();
|
|
114
|
+
growth = null;
|
|
115
|
+
}
|
|
116
|
+
if (!ease || growth) return;
|
|
117
|
+
growth = stage.animate([{ height: `${from}px` }, { height: `${px}px` }], { duration: GROW_MS, easing: GROW_EASE });
|
|
118
|
+
growthTo = px;
|
|
119
|
+
const run = growth;
|
|
120
|
+
run.finished.then(() => { if (growth === run) growth = null; }, () => {});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Back to the content's own height (under the cap).
|
|
124
|
+
function frameFree() {
|
|
125
|
+
growth?.cancel();
|
|
126
|
+
growth = null;
|
|
127
|
+
stage.style.height = "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function capPx() {
|
|
131
|
+
return parseFloat(stage.style.getPropertyValue("--stage-max")) || Infinity;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The scroller's content height: its last shown slot's end (offsetTop is
|
|
135
|
+
// the scroller's own, whatever its scroll) and its bottom padding.
|
|
136
|
+
function contentHeight() {
|
|
137
|
+
const css = getComputedStyle(scroller);
|
|
138
|
+
let end = parseFloat(css.paddingTop) || 0;
|
|
139
|
+
for (const el of scroller.children) {
|
|
140
|
+
if (el === holdEl || el.hidden || !el.getClientRects().length) continue;
|
|
141
|
+
end = el.offsetTop + el.offsetHeight;
|
|
142
|
+
}
|
|
143
|
+
return end + (parseFloat(css.paddingBottom) || 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Holds the frame while the stage holds a turn (stage_model.js
|
|
147
|
+
// frameTarget: grow-only, clamped by the cap; free when collapsed or
|
|
148
|
+
// released). Run from a ResizeObserver on the slots (after layout, before
|
|
149
|
+
// paint: no frame shows the content's own height) and when the cap moves.
|
|
150
|
+
function fitFrame() {
|
|
151
|
+
if (stage.hidden || stage.classList.contains("leaving")) return;
|
|
152
|
+
const content = scroller.hidden ? 0 : contentHeight();
|
|
153
|
+
const holding = isHolding();
|
|
154
|
+
const collapsed = isCollapsed();
|
|
155
|
+
const holds = holding && !collapsed;
|
|
156
|
+
maxContent = holds ? Math.max(maxContent, content) : 0;
|
|
157
|
+
const mark = `${Math.max(0, Math.round(maxContent) - 1)}px`;
|
|
158
|
+
if (holdEl.style.top !== mark) holdEl.style.top = mark;
|
|
159
|
+
const natural = (stage.offsetHeight - stage.clientHeight) + status.offsetHeight + content + (cloud.hidden || collapsed ? 0 : cloud.offsetHeight)
|
|
160
|
+
+ queuedSlot.offsetHeight;
|
|
161
|
+
held = frameTarget({ natural, held, cap: capPx(), holding, collapsed });
|
|
162
|
+
if (held === null) frameFree();
|
|
163
|
+
else frameTo(held, { animate: true });
|
|
164
|
+
}
|
|
165
|
+
const frameObserver = new ResizeObserver(() => fitFrame());
|
|
166
|
+
for (const el of observed) frameObserver.observe(el);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
fitCap,
|
|
170
|
+
frameFree,
|
|
171
|
+
afterGrowth,
|
|
172
|
+
easeStage,
|
|
173
|
+
reveal,
|
|
174
|
+
// A new turn, or none: no height held yet.
|
|
175
|
+
resetHeld: () => { held = null; },
|
|
176
|
+
// The tallest content seen is forgotten (a new turn, the cloud closed by hand).
|
|
177
|
+
forgetContent: () => { maxContent = 0; },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -15,9 +15,9 @@ const FILE_TOOLS = new Set(["read", "edit", "write"]);
|
|
|
15
15
|
const TASK_TOOLS = new Set(["task_wait", "task_get", "task_stop"]);
|
|
16
16
|
const TRAIL = 3;
|
|
17
17
|
// The hand-off waits this long after the stage was last in use.
|
|
18
|
-
|
|
18
|
+
const QUIET_MS = 1500;
|
|
19
19
|
// A touch, wheel or scroll counts as use this long (phones have no hover).
|
|
20
|
-
|
|
20
|
+
const TOUCH_MS = 4000;
|
|
21
21
|
|
|
22
22
|
// One headline sentence as plain text: the markdown a streaming answer
|
|
23
23
|
// carries (headings, quotes, bullets, table pipes, emphasis, code ticks,
|
|
@@ -124,6 +124,39 @@ export function trailFlashText(flash, mark) {
|
|
|
124
124
|
return { text, hover: text };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// The status row's mark for an ended turn's phase (liveSlots), none while
|
|
128
|
+
// it runs.
|
|
129
|
+
export const MARKS = { answered: "✓", "no answer": "∅", canceled: "■", failed: "✕" };
|
|
130
|
+
// The mark of a flash in the trail: a warn notice, a hook's info notice,
|
|
131
|
+
// an applied LLM context edit (✂), a steer-cut row and a plugin's nudge (↪).
|
|
132
|
+
export const FLASH_MARKS = { warn: "⚠", info: "ℹ", edit: "✂", steer: "↪" };
|
|
133
|
+
|
|
134
|
+
// "1 step", "3 steps".
|
|
135
|
+
export function stepsText(n) {
|
|
136
|
+
return `${n} step${n === 1 ? "" : "s"}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The chip's label: the steps, the calls, and the turn's time when the
|
|
140
|
+
// status row's timing line isn't there to say it (+elapsed+, "" for none).
|
|
141
|
+
export function chipLabelText({ steps, calls, elapsed = "" }) {
|
|
142
|
+
return `${stepsText(steps)} · ${calls} tool call${calls === 1 ? "" : "s"}${elapsed ? ` · ${elapsed}` : ""}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The status row's step: running, the step it is on; ended, the count,
|
|
146
|
+
// unless the chip says it (+chipHidden+: no chip shows).
|
|
147
|
+
export function stepSlotText({ step, ended, chipHidden }) {
|
|
148
|
+
return !ended ? `step ${step}` : chipHidden ? stepsText(step) : "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The collapsed stage's mini tool in the status row: the running call's
|
|
152
|
+
// name and title, its full command as the hover; "" for both otherwise.
|
|
153
|
+
export function miniToolText({ collapsed, tool }) {
|
|
154
|
+
return {
|
|
155
|
+
text: collapsed && tool ? `${tool.name} ${tool.title}` : "",
|
|
156
|
+
title: (collapsed && tool?.command) || "",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
127
160
|
// While the first step has no thinking and no tool rows the turn is plain:
|
|
128
161
|
// its text streams in the stage's answer slot, as the turn view's
|
|
129
162
|
// chrome-less block streams like a bubble.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The stage view's live slots (stage_view.js): the headline, the asking
|
|
2
|
+
// text, the "now" row, the trail and the chip's ticks, each drawn from
|
|
3
|
+
// liveSlots (stage_model.js) and patched only when what it shows changes
|
|
4
|
+
// (+shown+), so an unchanged slot keeps its nodes: a selection in it
|
|
5
|
+
// survives, and its animations never restart (the ticks' blink; the
|
|
6
|
+
// headline's fade restarts on purpose at a new text).
|
|
7
|
+
import { FLASH_MARKS, trailFlashText, trailItemText, trailTone } from "./stage_model.js";
|
|
8
|
+
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
9
|
+
|
|
10
|
+
// The stage's one element helper (stage_view.js builds its skeleton with it).
|
|
11
|
+
export function node(tag, cls, parent = null) {
|
|
12
|
+
const el = document.createElement(tag);
|
|
13
|
+
if (cls) el.className = cls;
|
|
14
|
+
if (parent) parent.appendChild(el);
|
|
15
|
+
return el;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// +headline+, +asking+, +now+, +trail+, +ticks+: the slots' elements.
|
|
19
|
+
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
20
|
+
// on a running task_wait; +gens()+ the running turn's gens (the button's
|
|
21
|
+
// command, read at its click); +onChange+ redraws (the stage's
|
|
22
|
+
// scheduleRefresh).
|
|
23
|
+
export function createStageSlots({ headline, asking, now, trail, ticks, taskStop = null, gens, onChange }) {
|
|
24
|
+
// What each slot shows, so an unchanged slot is left alone.
|
|
25
|
+
const shown = { headline: null, now: null, trail: null, asking: null };
|
|
26
|
+
|
|
27
|
+
function drawHeadline({ text, thinking }) {
|
|
28
|
+
const key = `${thinking ? "t" : "n"}|${text}`;
|
|
29
|
+
if (shown.headline === key) return;
|
|
30
|
+
shown.headline = key;
|
|
31
|
+
headline.replaceChildren();
|
|
32
|
+
headline.classList.toggle("thinking", thinking);
|
|
33
|
+
if (thinking) {
|
|
34
|
+
const tag = node("span", "ts-think-tag", headline);
|
|
35
|
+
tag.textContent = "thinking";
|
|
36
|
+
}
|
|
37
|
+
headline.appendChild(document.createTextNode(text));
|
|
38
|
+
headline.classList.remove("fresh");
|
|
39
|
+
void headline.offsetWidth;
|
|
40
|
+
headline.classList.add("fresh");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Patched only when the text changes (a late chunk to the closed gen
|
|
44
|
+
// rewrites the same text): a selection in it survives. A new text reads
|
|
45
|
+
// from its top.
|
|
46
|
+
function drawAsking(text) {
|
|
47
|
+
asking.hidden = !text;
|
|
48
|
+
if (shown.asking === text) return;
|
|
49
|
+
shown.asking = text;
|
|
50
|
+
asking.textContent = text;
|
|
51
|
+
asking.scrollTop = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function drawNow(tool, quiet) {
|
|
55
|
+
// A task_wait's stop button, as the row's (task_stop.js): in the key
|
|
56
|
+
// with its in-flight state, so a redraw keeps a clicked one disabled.
|
|
57
|
+
const stopId = tool?.taskId && taskStop?.target({ tool: "task_wait", status: "running", view: { task_id: tool.taskId } });
|
|
58
|
+
const stopKey = stopId ? `|stop:${tool.key}:${taskStop.busy(stopId)}` : "";
|
|
59
|
+
const key = tool ? `run|${tool.name}|${tool.title}|${tool.command || ""}${stopKey}` : `quiet|${quiet}`;
|
|
60
|
+
if (shown.now === key) return;
|
|
61
|
+
shown.now = key;
|
|
62
|
+
now.replaceChildren();
|
|
63
|
+
now.classList.toggle("running", !!tool);
|
|
64
|
+
node("span", tool ? "ts-spin" : "ts-idle", now);
|
|
65
|
+
if (tool) {
|
|
66
|
+
const name = node("span", `ts-tool ${tool.kind}`, now);
|
|
67
|
+
name.textContent = tool.name;
|
|
68
|
+
const title = node("span", "ts-title", now);
|
|
69
|
+
title.textContent = tool.title;
|
|
70
|
+
// The full command when the call has one (the title is its first step +N).
|
|
71
|
+
title.title = tool.command || tool.title;
|
|
72
|
+
if (stopId) {
|
|
73
|
+
const command = () => taskCommand(gens(), stopId);
|
|
74
|
+
now.appendChild(taskStopButton(taskStop, stopId, { command, onChange }));
|
|
75
|
+
}
|
|
76
|
+
} else if (quiet) {
|
|
77
|
+
const word = node("span", "ts-title quiet", now);
|
|
78
|
+
word.textContent = quiet;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// +flash+: the trail's flash ({ kind, text, until }), shown ahead of the
|
|
83
|
+
// calls until its time is up.
|
|
84
|
+
function drawTrail(items, flash) {
|
|
85
|
+
const flashing = flash && flash.until > Date.now() ? flash : null;
|
|
86
|
+
const key = JSON.stringify([flashing?.kind, flashing?.text, items]);
|
|
87
|
+
if (shown.trail === key) return;
|
|
88
|
+
shown.trail = key;
|
|
89
|
+
trail.replaceChildren();
|
|
90
|
+
if (flashing) {
|
|
91
|
+
const flash = trailFlashText(flashing, FLASH_MARKS[flashing.kind]);
|
|
92
|
+
const el = node("span", `ts-trail-item ${flashing.kind}`, trail);
|
|
93
|
+
el.textContent = flash.text;
|
|
94
|
+
el.title = flash.hover;
|
|
95
|
+
}
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
const tone = trailTone(item.status);
|
|
98
|
+
const el = node("span", `ts-trail-item${tone ? ` ${tone}` : ""}`, trail);
|
|
99
|
+
const { text, hover } = trailItemText(item);
|
|
100
|
+
el.textContent = text;
|
|
101
|
+
el.title = hover;
|
|
102
|
+
}
|
|
103
|
+
if (!flashing && !items.length) node("span", "ts-trail-item none", trail).textContent = "no tool calls yet";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function drawTicks(list) {
|
|
107
|
+
list.forEach((tick, i) => {
|
|
108
|
+
let el = ticks.children[i];
|
|
109
|
+
if (!el) {
|
|
110
|
+
el = node("span", "", ticks);
|
|
111
|
+
el.setAttribute("role", "button");
|
|
112
|
+
}
|
|
113
|
+
const cls = `ts-tick ${tick.kind} ${tick.status}`;
|
|
114
|
+
if (el.className !== cls) el.className = cls;
|
|
115
|
+
el.dataset.step = String(tick.step);
|
|
116
|
+
el.title = `step ${tick.step + 1}`;
|
|
117
|
+
});
|
|
118
|
+
while (ticks.children.length > list.length) ticks.lastChild.remove();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A new turn (or none): the slots empty, the asking text hidden, the next
|
|
122
|
+
// draw of each slot a fresh one.
|
|
123
|
+
function reset() {
|
|
124
|
+
for (const slot of [headline, asking, now, trail, ticks]) slot.replaceChildren();
|
|
125
|
+
asking.hidden = true;
|
|
126
|
+
shown.headline = shown.now = shown.trail = shown.asking = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { drawHeadline, drawAsking, drawNow, drawTrail, drawTicks, reset };
|
|
130
|
+
}
|