samagotchi 0.64.0 → 0.66.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 +88 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/architecture.md +20 -3
- data/docs/cli.md +36 -2
- data/docs/configuration.md +177 -8
- data/docs/context.md +4 -1
- data/docs/internals/engine.md +19 -4
- data/docs/internals/prompt-caching.md +10 -2
- data/docs/memory.md +1 -1
- data/docs/plugins.md +163 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +13 -2
- data/docs/sub-agent.md +3 -1
- data/lib/samagotchi/bridge.rb +11 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +18 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +21 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +464 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +131 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +68 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/client.rb +4 -2
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +13 -5
- data/lib/samagotchi/engine.rb +151 -11
- data/lib/samagotchi/generation_phase.rb +17 -0
- data/lib/samagotchi/host_config.rb +27 -6
- data/lib/samagotchi/host_registry.rb +34 -11
- data/lib/samagotchi/idle_client.rb +62 -25
- data/lib/samagotchi/idle_recap.rb +5 -2
- data/lib/samagotchi/idle_target.rb +30 -7
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/adapters.rb +41 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +212 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
- data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
- data/lib/samagotchi/llm/api_key.rb +26 -9
- data/lib/samagotchi/llm/chat_loop.rb +113 -14
- data/lib/samagotchi/llm/dialect.rb +57 -0
- data/lib/samagotchi/llm/errors.rb +81 -12
- data/lib/samagotchi/llm/model_listing.rb +111 -0
- data/lib/samagotchi/llm/model_result.rb +9 -2
- data/lib/samagotchi/llm/openai_chat.rb +46 -93
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +80 -7
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/reasoning_history.rb +14 -4
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/self_report.rb +18 -28
- data/lib/samagotchi/send_command.rb +23 -7
- data/lib/samagotchi/session.rb +21 -3
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_metrics.rb +6 -11
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/steer_cut.rb +4 -0
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +8 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/turn_note.rb +22 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -1
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +111 -20
- data/lib/samagotchi/web/public/commands.js +4 -2
- data/lib/samagotchi/web/public/delivery_line.js +228 -0
- data/lib/samagotchi/web/public/index.html +38 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/live_turn.js +8 -0
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_model.js +27 -7
- data/lib/samagotchi/web/public/stage_view.js +32 -10
- data/lib/samagotchi/web/public/stream_handlers.js +21 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/web/public/turn_events.js +22 -8
- data/lib/samagotchi/web/public/turn_html.js +20 -4
- data/lib/samagotchi/web/public/turn_model.js +16 -9
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/worker.rb +2 -0
- metadata +17 -1
|
@@ -480,6 +480,15 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
480
480
|
#annotateBar{position:fixed;z-index:20;display:flex;flex-wrap:wrap;gap:4px;max-width:calc(100vw - 16px)}
|
|
481
481
|
#annotateBar[hidden]{display:none}
|
|
482
482
|
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px var(--shadow-strong)}
|
|
483
|
+
/* A bundle's pills (chi.annotation) after a thin separator; beyond 3, "⋯" opens the rest in a list under it. */
|
|
484
|
+
#annotateBar .annotate-actions{display:flex;flex-wrap:wrap;gap:4px;align-items:center;position:relative}
|
|
485
|
+
#annotateBar .annotate-actions[hidden]{display:none}
|
|
486
|
+
#annotateBar .annotate-sep{width:1px;align-self:stretch;margin:3px 2px;background:var(--border-strong)}
|
|
487
|
+
#annotateBar .annotate-action,#annotateBar .annotate-more{background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border);padding:3px 10px;max-width:32ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
488
|
+
#annotateBar .annotate-action:disabled{opacity:.5;cursor:not-allowed}
|
|
489
|
+
#annotateBar .annotate-action[data-sends]::after{content:"↗";margin-left:4px;opacity:.75}
|
|
490
|
+
#annotateBar .annotate-more-list{position:absolute;top:calc(100% + 4px);right:0;display:flex;flex-direction:column;align-items:flex-end;gap:4px}
|
|
491
|
+
#annotateBar .annotate-more-list[hidden]{display:none}
|
|
483
492
|
#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}
|
|
484
493
|
/* 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. */
|
|
485
494
|
.bubble.output,.bubble.user{position:relative}
|
|
@@ -511,6 +520,30 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
511
520
|
#composer{display:flex;flex-direction:column;gap:8px;padding:14px 14px 10px}
|
|
512
521
|
#composer .compose-col{flex:1;display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
513
522
|
#composer.dropping{outline:2px dashed var(--accent);outline-offset:-4px}
|
|
523
|
+
/* The delivery line (delivery_line.js): a session's row above the text field, always there so the composer never changes height: the key hint while idle; while a turn runs, where a message typed now goes in, then the cut and queue links. One quiet row, ellipsised; a phone gets the short text and chips. */
|
|
524
|
+
#deliveryLine{display:flex;align-items:center;gap:10px;min-width:0;flex:1 0 100%;height:20px;padding:0 2px;overflow:hidden;font-size:12px;line-height:18px;color:var(--text-muted);user-select:none}
|
|
525
|
+
#deliveryLine[hidden]{display:none}
|
|
526
|
+
#deliveryLine .delivery-where{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
527
|
+
#deliveryLine .delivery-where.idle{color:var(--text-faint)}
|
|
528
|
+
#deliveryLine .short{display:none}
|
|
529
|
+
#deliveryLine .delivery-links{flex:none;display:flex;align-items:center;gap:6px;white-space:nowrap}
|
|
530
|
+
#deliveryLine .delivery-sep{color:var(--text-faint)}
|
|
531
|
+
#deliveryLine .delivery-link{background:transparent;border:0;padding:0 2px;min-height:0;height:18px;font:inherit;line-height:18px;color:var(--accent);cursor:pointer;border-radius:4px}
|
|
532
|
+
#deliveryLine .delivery-link:hover:not(:disabled):not(.armed){text-decoration:underline;filter:none}
|
|
533
|
+
#deliveryLine .delivery-link:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
534
|
+
#deliveryLine .delivery-link:disabled{color:var(--text-faint);opacity:1;cursor:default}
|
|
535
|
+
#deliveryLine .delivery-link.armed{color:var(--on-solid);background:var(--accent-strong);padding:0 6px}
|
|
536
|
+
/* In a session the composer is a row: the line's row wraps above the text field and the buttons. */
|
|
537
|
+
body.has-session #composer{flex-wrap:wrap;row-gap:6px}
|
|
538
|
+
@media(max-width:600px){
|
|
539
|
+
#deliveryLine .long,#deliveryLine .delivery-sep{display:none}
|
|
540
|
+
#deliveryLine .short{display:inline}
|
|
541
|
+
#deliveryLine .delivery-link{padding:0 8px;border:1px solid var(--accent-border);border-radius:999px;background:var(--accent-bg);line-height:16px}
|
|
542
|
+
#deliveryLine .delivery-link:disabled{border-color:var(--border);background:transparent}
|
|
543
|
+
#deliveryLine .delivery-link.armed{border-color:var(--accent-strong);background:var(--accent-strong)}
|
|
544
|
+
}
|
|
545
|
+
/* A line queued for after the running turn: a dashed bubble, a turn of its own to come. */
|
|
546
|
+
.bubble.user.queued[data-delivery="queue"]{border:1px dashed var(--accent-border);border-left:3px dashed var(--accent);background:transparent}
|
|
514
547
|
#chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
515
548
|
#chips[hidden]{display:none}
|
|
516
549
|
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--accent-bg);border:1px solid var(--border-strong);border-radius:8px;padding:3px 4px 3px 3px;font-size:12px;max-width:220px}
|
|
@@ -718,6 +751,9 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
718
751
|
/* The cloud: one chip (steps, calls, a tick per call), the turn view's block behind it. */
|
|
719
752
|
/* The footer: the chip at the stage's bottom edge (the cloud's steps open above it, the scroller's last slot). */
|
|
720
753
|
.ts-foot{flex:none;padding:0 14px 10px}
|
|
754
|
+
/* Below the turn's card: the lines queued for after it (each a turn of its own), apart from the turn by a dashed rule. */
|
|
755
|
+
.ts-queued{flex:none;display:flex;flex-direction:column;gap:6px;padding:8px 14px 10px;border-top:1px dashed var(--border-strong);max-height:calc(var(--stage-max,60vh) * .3);overflow:auto;scrollbar-width:thin}
|
|
756
|
+
.ts-queued:empty{display:none}
|
|
721
757
|
.ts-foot[hidden],.turn-stage.collapsed .ts-foot{display:none}
|
|
722
758
|
.ts-chip{display:flex;align-items:center;gap:10px;flex-wrap:nowrap;width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:9px;background:transparent;color:var(--text-muted);text-align:left}
|
|
723
759
|
.ts-chip:hover:not(:disabled){filter:none;background:var(--bg-hover)}
|
|
@@ -750,6 +786,7 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
750
786
|
.ts-status{padding-left:10px}
|
|
751
787
|
.ts-scroll{padding:8px 10px 10px}
|
|
752
788
|
.ts-foot{padding:0 10px 10px}
|
|
789
|
+
.ts-queued{padding:8px 10px 10px}
|
|
753
790
|
.ts-defer{display:none}
|
|
754
791
|
.ts-trail-item{max-width:24ch}
|
|
755
792
|
}
|
|
@@ -1132,6 +1169,7 @@ a{color:var(--accent)}
|
|
|
1132
1169
|
<div id="modelList" role="listbox" aria-label="Models"></div>
|
|
1133
1170
|
</div>
|
|
1134
1171
|
<div id="composeGrip" title="Drag to make the message box taller (double-click resets)"></div>
|
|
1172
|
+
<div id="deliveryLine" aria-live="off" hidden></div>
|
|
1135
1173
|
<div class="compose-col">
|
|
1136
1174
|
<div id="chips" hidden></div>
|
|
1137
1175
|
<textarea id="prompt" placeholder="Ask chi anything…" aria-controls="commandList" aria-autocomplete="list" aria-expanded="false"></textarea>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The session bar's info line (#infoText): the open session's id, its copy
|
|
2
2
|
// chi --attach button, status, model, chain links, parent and delegates,
|
|
3
|
-
// llm ctx, thinking
|
|
3
|
+
// llm ctx, thinking, reasoning history and tools chips, model notes, memories, folder, ctx, speed,
|
|
4
4
|
// session clock and the worker's version badge. A lit-html template over a
|
|
5
5
|
// plain model app.js builds (updateInfoBar), so the markup lives here and
|
|
6
6
|
// runs under node --test (spec/web/public/info_bar.test.js).
|
|
@@ -16,6 +16,7 @@ import { ALL_SESSIONS_HASH, sessionHash } from "./route.js";
|
|
|
16
16
|
import { chipTemplate as llmContextChip } from "./llm_context_chip.js";
|
|
17
17
|
import { chipTemplate as thinkingChip } from "./thinking_chip.js";
|
|
18
18
|
import { chipTemplate as reasoningHistoryChip } from "./reasoning_history_chip.js";
|
|
19
|
+
import { chipTemplate as toolkitChip } from "./toolkit_chip.js";
|
|
19
20
|
|
|
20
21
|
export const PREV = "←";
|
|
21
22
|
export const NEXT = "→";
|
|
@@ -62,7 +63,8 @@ function kidsTemplate(kids) {
|
|
|
62
63
|
// mismatch}: format.js modelLabel), chain ({prev, next}: chainLinkTemplate's
|
|
63
64
|
// links or null), parent ({id, title} or null), kids (format.js
|
|
64
65
|
// childrenSummary), llmContext, thinking and reasoningHistory (the chips'
|
|
65
|
-
// summaries; reasoningHistory null on a native host: no chip),
|
|
66
|
+
// summaries; reasoningHistory null on a native host: no chip), toolkit
|
|
67
|
+
// (the tools chip's summary, null on full: no chip), notes
|
|
66
68
|
// ({text, title} or null), memories (the used names), dir, ctx ({text,
|
|
67
69
|
// tip}), speed ({text, tip} or null), clock (the session time's text, ""
|
|
68
70
|
// for none), worker ({badge, busy} or null).
|
|
@@ -70,7 +72,7 @@ export function infoBarHtml(bar) {
|
|
|
70
72
|
if (!bar) return nothing;
|
|
71
73
|
const attach = `chi --attach ${bar.id}`;
|
|
72
74
|
const memories = bar.memories || [];
|
|
73
|
-
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
75
|
+
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${toolkitChip(bar.toolkit)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip.
|
|
@@ -26,6 +26,12 @@ export function createLiveTurn(timingDeps) {
|
|
|
26
26
|
displayPending: null,
|
|
27
27
|
// The last turn_failed's line, shown again after prompt_restored's resync.
|
|
28
28
|
failedText: null,
|
|
29
|
+
// The run's cut wait in seconds (turn_started / the snapshot's
|
|
30
|
+
// cut_after; null: unknown, an older worker) and, on a join
|
|
31
|
+
// mid-generation, the snapshot's generation {thinking_only, age, at}
|
|
32
|
+
// until the next generation starts: the composer's delivery line.
|
|
33
|
+
cutAfter: null,
|
|
34
|
+
serverGen: null,
|
|
29
35
|
|
|
30
36
|
releaseDisplay() {
|
|
31
37
|
this.displayPending?.finish();
|
|
@@ -39,6 +45,8 @@ export function createLiveTurn(timingDeps) {
|
|
|
39
45
|
this.unmatchedMerge = false;
|
|
40
46
|
this.wakeStart = false;
|
|
41
47
|
this.failedText = null;
|
|
48
|
+
this.cutAfter = null;
|
|
49
|
+
this.serverGen = null;
|
|
42
50
|
this.releaseDisplay();
|
|
43
51
|
},
|
|
44
52
|
};
|
|
@@ -48,6 +48,7 @@ export function emptySessionState() {
|
|
|
48
48
|
// Which of its reasoning the model gets back from the next turn on, with
|
|
49
49
|
// its source (the reasoning history chip), or null (a native host).
|
|
50
50
|
reasoningHistory: null,
|
|
51
|
+
toolkit: null,
|
|
51
52
|
timing: normalizeTiming(),
|
|
52
53
|
};
|
|
53
54
|
}
|
|
@@ -83,6 +84,8 @@ export function sessionStateFrom(data) {
|
|
|
83
84
|
state.thinking = s.thinking || null;
|
|
84
85
|
// null on a native host (it ignores the level) and from an older worker.
|
|
85
86
|
state.reasoningHistory = s.reasoning_history || null;
|
|
87
|
+
// The tools chip's (null on full).
|
|
88
|
+
state.toolkit = s.toolkit || null;
|
|
86
89
|
state.parentId = s.parent_id || null;
|
|
87
90
|
state.continues = s.continues || null;
|
|
88
91
|
state.commands = Array.isArray(data.commands) ? data.commands : [];
|
|
@@ -222,14 +222,30 @@ export function handOffDue({ ended, pending, inUse, quietSinceMs, nowMs }) {
|
|
|
222
222
|
// A handed-off turn's nodes in the order the turn view leaves them: the
|
|
223
223
|
// prompt bubble (none for a reminder turn), the block, the extras in
|
|
224
224
|
// arrival order, the answer's pieces, the timing line, the end line, the
|
|
225
|
-
// kept cards.
|
|
226
|
-
export function handOffOrder({ prompt = null, block = null, extras = [], answer = [], timing = null, end = null, kept = [] }) {
|
|
227
|
-
return [prompt, block, ...extras, ...answer, timing, end, ...kept].filter(Boolean);
|
|
225
|
+
// kept cards, then the lines queued for after the turn (they run next).
|
|
226
|
+
export function handOffOrder({ prompt = null, block = null, extras = [], answer = [], timing = null, end = null, kept = [], queued = [] }) {
|
|
227
|
+
return [prompt, block, ...extras, ...answer, timing, end, ...kept, ...queued].filter(Boolean);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
230
|
+
// The rows that wait for the running turn's end to run as their own: a
|
|
231
|
+
// prompt queued for after the turn (its delivery queue: the composer's
|
|
232
|
+
// queue, /queue, chi send --queue, an image line) and a command queued
|
|
233
|
+
// for the turn's end (commands.js drawCommandBubble's queued).
|
|
234
|
+
export const QUEUED_ROWS = '.bubble.user.queued[data-delivery="queue"], .bubble.command.queued';
|
|
235
|
+
|
|
236
|
+
// QUEUED_ROWS among +parent+'s own children (querySelectorAll on it).
|
|
237
|
+
export const QUEUED_CHILDREN = QUEUED_ROWS.split(", ").map((sel) => `:scope > ${sel}`).join(", ");
|
|
238
|
+
|
|
239
|
+
// The kind placeFor reads off an element app.js places.
|
|
240
|
+
export function placeKind(el) {
|
|
241
|
+
if (el.matches?.(".bubble.recap")) return "recap";
|
|
242
|
+
if (el.matches?.(QUEUED_ROWS)) return "queued";
|
|
243
|
+
return "row";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Where a row app.js would append goes (placeFor, at the end of this
|
|
247
|
+
// file): the stage's extras while a turn is live (and not handing off),
|
|
248
|
+
// else the history; the recap always the history (it is idle-only).
|
|
233
249
|
// What flashes in the trail for a few seconds while the turn runs (the
|
|
234
250
|
// rows themselves go into their step inside the closed cloud): a hook's
|
|
235
251
|
// notice, warn or info, as its row reads, the ✂ row of an applied LLM
|
|
@@ -258,7 +274,11 @@ export function flashOf(what, data = {}) {
|
|
|
258
274
|
return { kind: data.level === "warn" ? "warn" : "info", text: data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}` };
|
|
259
275
|
}
|
|
260
276
|
|
|
277
|
+
// A queued row ("queued", placeKind) goes after the running turn, not
|
|
278
|
+
// into it: the stage's queued slot below the turn's card while one is
|
|
279
|
+
// live, else the history's end (after the turn there too).
|
|
261
280
|
export function placeFor({ live, handingOff, kind }) {
|
|
262
281
|
if (kind === "recap") return "history";
|
|
263
|
-
|
|
282
|
+
if (!live || handingOff) return "history";
|
|
283
|
+
return kind === "queued" ? "queued" : "stage";
|
|
264
284
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// reload. Slots are patched in place, never rebuilt from HTML: a selection
|
|
14
14
|
// and annotate survive. The rules are pure in stage_model.js.
|
|
15
15
|
import { createTurnView } from "./turn_view.js";
|
|
16
|
-
import { flashOf, frameTarget, handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor, trailFlashText, trailItemText, trailTone } from "./stage_model.js";
|
|
16
|
+
import { flashOf, frameTarget, handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor, placeKind, QUEUED_CHILDREN, trailFlashText, trailItemText, trailTone } from "./stage_model.js";
|
|
17
17
|
import { formatDuration } from "./timing.js";
|
|
18
18
|
import { easeScrollTo } from "./scroll.js";
|
|
19
19
|
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
@@ -124,6 +124,9 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
124
124
|
chip.type = "button";
|
|
125
125
|
const chipLabel = node("span", "ts-chip-label", chip);
|
|
126
126
|
const ticks = node("span", "ts-ticks", chip);
|
|
127
|
+
// Below the turn's card: the lines queued for after it (placeKind
|
|
128
|
+
// "queued"), each to run as a turn of its own; they hand off last.
|
|
129
|
+
const queuedSlot = node("div", "ts-queued", stage);
|
|
127
130
|
|
|
128
131
|
// The turn in the stage, from turnStarted until the hand-off (or a reset).
|
|
129
132
|
let holding = false;
|
|
@@ -600,13 +603,14 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
600
603
|
maxContent = holds ? Math.max(maxContent, content) : 0;
|
|
601
604
|
const mark = `${Math.max(0, Math.round(maxContent) - 1)}px`;
|
|
602
605
|
if (holdEl.style.top !== mark) holdEl.style.top = mark;
|
|
603
|
-
const natural = (stage.offsetHeight - stage.clientHeight) + status.offsetHeight + content + (cloud.hidden || collapsed ? 0 : cloud.offsetHeight)
|
|
606
|
+
const natural = (stage.offsetHeight - stage.clientHeight) + status.offsetHeight + content + (cloud.hidden || collapsed ? 0 : cloud.offsetHeight)
|
|
607
|
+
+ queuedSlot.offsetHeight;
|
|
604
608
|
held = frameTarget({ natural, held, cap: capPx(), holding, collapsed });
|
|
605
609
|
if (held === null) frameFree();
|
|
606
610
|
else frameTo(held, { animate: true });
|
|
607
611
|
}
|
|
608
612
|
const frameObserver = new ResizeObserver(() => fitFrame());
|
|
609
|
-
for (const el of [status, promptSlot, live, extras, answer, tail, cloudBody, cloud]) frameObserver.observe(el);
|
|
613
|
+
for (const el of [status, promptSlot, live, extras, answer, tail, cloudBody, cloud, queuedSlot]) frameObserver.observe(el);
|
|
610
614
|
|
|
611
615
|
// ── in use: pointer over it (tracked on the stage itself: :hover goes
|
|
612
616
|
// stale when children change), a touch or wheel in the last 4 s (phones
|
|
@@ -677,7 +681,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
677
681
|
}
|
|
678
682
|
|
|
679
683
|
function emptySlots() {
|
|
680
|
-
for (const slot of [promptSlot, extras, answer, tail, cloudBody, timingSlot, headline, asking, now, trail, ticks]) slot.replaceChildren();
|
|
684
|
+
for (const slot of [promptSlot, extras, answer, tail, cloudBody, queuedSlot, timingSlot, headline, asking, now, trail, ticks]) slot.replaceChildren();
|
|
681
685
|
promptSlot.classList.remove("full");
|
|
682
686
|
asking.hidden = true;
|
|
683
687
|
shown.headline = shown.now = shown.trail = shown.asking = null;
|
|
@@ -744,6 +748,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
744
748
|
answer: [...answer.children],
|
|
745
749
|
timing: timingSlot.firstElementChild,
|
|
746
750
|
kept: [...tail.children],
|
|
751
|
+
queued: [...queuedSlot.children],
|
|
747
752
|
});
|
|
748
753
|
const animate = !immediate && !reducedMotion() && !document.hidden && !collapsed;
|
|
749
754
|
folding = { pieces, block: blockEl, wasAtEnd: historyNearBottom(), animate };
|
|
@@ -779,6 +784,9 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
779
784
|
return {
|
|
780
785
|
turnStarted(data) {
|
|
781
786
|
if (folding) finishFold();
|
|
787
|
+
// A turn that never ended (released, not handed off) leaves its queued
|
|
788
|
+
// lines waiting still: they stay.
|
|
789
|
+
const waiting = [...queuedSlot.children].filter((el) => el !== data?.promptBubble);
|
|
782
790
|
if (holding) {
|
|
783
791
|
if (endKind) handOff({ immediate: true });
|
|
784
792
|
else release();
|
|
@@ -794,15 +802,19 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
794
802
|
// The turn's prompt bubble (app.js found or drew it) sits in the prompt
|
|
795
803
|
// line until the hand-off takes it back first.
|
|
796
804
|
if (data?.promptBubble) promptSlot.appendChild(data.promptBubble);
|
|
805
|
+
// The lines still queued behind this turn went into the history with
|
|
806
|
+
// the turn before (the hand-off at this turn_started, or an earlier
|
|
807
|
+
// one): back below this turn's card, as a reload places them.
|
|
808
|
+
queuedSlot.append(...waiting.filter((el) => !el.isConnected), ...historyEl.querySelectorAll(QUEUED_CHILDREN));
|
|
797
809
|
view.turnStarted(data || {});
|
|
798
810
|
show();
|
|
799
811
|
refresh();
|
|
800
812
|
},
|
|
801
|
-
generationStarted(data) {
|
|
813
|
+
generationStarted(data, now = null) {
|
|
802
814
|
// The next step: the live slots' waiting height is let go.
|
|
803
815
|
stage.classList.remove("held-wait");
|
|
804
816
|
stage.style.removeProperty("--live-held");
|
|
805
|
-
view.generationStarted(data);
|
|
817
|
+
view.generationStarted(data, now);
|
|
806
818
|
},
|
|
807
819
|
chunk: (data) => view.chunk(data),
|
|
808
820
|
generationRestarted: (data) => view.generationRestarted(data),
|
|
@@ -853,18 +865,28 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
853
865
|
},
|
|
854
866
|
containsLive: (el) => view.containsLive(el),
|
|
855
867
|
askingText: () => view.askingText(),
|
|
868
|
+
// The turn model and its live phase as the status row reads it (the
|
|
869
|
+
// composer's delivery line).
|
|
870
|
+
current: () => view.current(),
|
|
871
|
+
phase: () => liveSlots(view.current().turn, { pending: pendingCard() || asksCard() }).phase,
|
|
856
872
|
reset() {
|
|
857
873
|
if (folding) finishFold();
|
|
858
874
|
release();
|
|
859
875
|
},
|
|
860
|
-
// The stage's extras while it holds a turn (a card, a
|
|
861
|
-
// command, a note, the end line)
|
|
876
|
+
// The stage's extras while it holds a turn (a card, a prompt for its
|
|
877
|
+
// next step, a command, a note, the end line), its queued slot for a
|
|
878
|
+
// line queued for after it; false: the history takes it.
|
|
862
879
|
place(el) {
|
|
863
880
|
// A turn still sliding away goes into the history first: what comes
|
|
864
881
|
// now comes after it.
|
|
865
882
|
if (folding) finishFold();
|
|
866
|
-
const
|
|
867
|
-
if (
|
|
883
|
+
const where = placeFor({ live: holding, handingOff: false, kind: placeKind(el) });
|
|
884
|
+
if (where === "history") return false;
|
|
885
|
+
if (where === "queued") {
|
|
886
|
+
queuedSlot.appendChild(el);
|
|
887
|
+
scheduleRefresh();
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
868
890
|
const slot = endKind ? tail : extras;
|
|
869
891
|
const stick = slot.scrollHeight - slot.scrollTop - slot.clientHeight < 24;
|
|
870
892
|
slot.appendChild(el);
|
|
@@ -64,7 +64,7 @@ export function createStreamHandlers(deps) {
|
|
|
64
64
|
// chunk_router.js). Raw `content` is only a fallback for an older server.
|
|
65
65
|
const { text, thinking } = routeChunk(data);
|
|
66
66
|
liveTurn.timing.setNote("");
|
|
67
|
-
turnDom.chunk({ text, thinking, iteration: data.iteration ?? null });
|
|
67
|
+
turnDom.chunk({ text, thinking, iteration: data.iteration ?? null, now: data.replayed ? null : Date.now() });
|
|
68
68
|
const current = state();
|
|
69
69
|
const pct = extractCtxPct(data, current.ctxWindow, current.llmContext?.budget_tokens);
|
|
70
70
|
if (pct !== null && !Number.isNaN(pct)) {
|
|
@@ -79,7 +79,11 @@ export function createStreamHandlers(deps) {
|
|
|
79
79
|
current.ctxWindowSource = data.context_window_source || null;
|
|
80
80
|
}
|
|
81
81
|
liveTurn.timing.setNote("");
|
|
82
|
-
|
|
82
|
+
// The step's clock (the composer's delivery line): the page's own at a
|
|
83
|
+
// live start; a replay's comes from the snapshot's generation age,
|
|
84
|
+
// which a live start makes stale.
|
|
85
|
+
if (!data?.replayed) liveTurn.serverGen = null;
|
|
86
|
+
turnDom.generationStarted(data || {}, data?.replayed ? null : Date.now());
|
|
83
87
|
},
|
|
84
88
|
generation_completed: (data) => {
|
|
85
89
|
liveTurn.timing.setNote("");
|
|
@@ -115,6 +119,8 @@ export function createStreamHandlers(deps) {
|
|
|
115
119
|
markRecapStale();
|
|
116
120
|
shownTexts.turnStarted();
|
|
117
121
|
const promptBubble = applyPromptOps(data);
|
|
122
|
+
liveTurn.cutAfter = data.cut_after ?? null;
|
|
123
|
+
liveTurn.serverGen = null;
|
|
118
124
|
setTurnRunning(true);
|
|
119
125
|
setLiveStatus("running");
|
|
120
126
|
liveTurn.timing.detach();
|
|
@@ -223,6 +229,11 @@ export function createStreamHandlers(deps) {
|
|
|
223
229
|
updateInfoBar();
|
|
224
230
|
reasoningHistoryControl.refresh();
|
|
225
231
|
}
|
|
232
|
+
// /toolkit changed what the next turn declares; null: full (no chip).
|
|
233
|
+
if ("toolkit" in data) {
|
|
234
|
+
state().toolkit = data.toolkit || null;
|
|
235
|
+
updateInfoBar();
|
|
236
|
+
}
|
|
226
237
|
if (view.resync) await resync();
|
|
227
238
|
// A card's action that isn't anytime (the step-limit question's
|
|
228
239
|
// /continue) has no bubble: nothing to remove. An anytime or queued
|
|
@@ -240,6 +251,9 @@ export function createStreamHandlers(deps) {
|
|
|
240
251
|
// message renders as one) — no second bubble, or the output shows twice.
|
|
241
252
|
else if (view.resync && historyHasCommandBubble(findInTurnRoots(".bubble.command"), view.line)) { /* drawn by the resync */ }
|
|
242
253
|
else addCommandBubble(view);
|
|
254
|
+
// This tab's command refused as busy (a plugin's command mid-turn): the
|
|
255
|
+
// composer was cleared when it went; its line comes back to send later.
|
|
256
|
+
if (view.own && view.busy && composer.empty()) composer.refill(view.line);
|
|
243
257
|
},
|
|
244
258
|
// An anytime command (/help, a plugin's /btw) runs at once, beside a
|
|
245
259
|
// turn: its bubble goes here, so the cards it shows come after it. One
|
|
@@ -295,6 +309,11 @@ export function createStreamHandlers(deps) {
|
|
|
295
309
|
// shows elsewhere (a hook's links in the rendered answer) isn't drawn,
|
|
296
310
|
// not even in the stage trail; a joined turn's notices come here too.
|
|
297
311
|
hook_notice: (data) => {
|
|
312
|
+
// A plugin put the session on another toolkit (ctx.use_toolkit).
|
|
313
|
+
if ("toolkit" in data) {
|
|
314
|
+
state().toolkit = data.toolkit || null;
|
|
315
|
+
updateInfoBar();
|
|
316
|
+
}
|
|
298
317
|
if (shownElsewhere(data, capabilities())) return;
|
|
299
318
|
if (!turnDom.notice(data)) showHookNotice(data);
|
|
300
319
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The session bar's tools chip: the session's toolkit when it isn't full
|
|
2
|
+
// ("tools lean"), with the tools it leaves out and how to change them in
|
|
3
|
+
// its tooltip. The summary is the session's toolkit ({name, label,
|
|
4
|
+
// left_out, fallback}: Toolkit::Explained#summary; the worker's snapshot or
|
|
5
|
+
// a command_ran; the server's own reading of the session file has no
|
|
6
|
+
// left_out). Changing it is /toolkit (no popover yet).
|
|
7
|
+
|
|
8
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
9
|
+
|
|
10
|
+
// The chip's text and tooltip; null without a summary (full).
|
|
11
|
+
export function chipModel(summary) {
|
|
12
|
+
if (!summary) return null;
|
|
13
|
+
const label = summary.label || summary.name;
|
|
14
|
+
const parts = [`Toolkit: ${label}, the tools this session declares.`];
|
|
15
|
+
if (summary.fallback) parts.push(`${summary.fallback}: every tool is declared (full) until it resolves.`);
|
|
16
|
+
if (Array.isArray(summary.left_out)) {
|
|
17
|
+
parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
|
|
18
|
+
}
|
|
19
|
+
parts.push("/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.");
|
|
20
|
+
return { text: `tools ${label}${summary.fallback ? " ?" : ""}`, title: parts.join(" ") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The session bar's chip (a lit-html template; info_bar.js), nothing on
|
|
24
|
+
// full.
|
|
25
|
+
export function chipTemplate(summary) {
|
|
26
|
+
const chip = chipModel(summary);
|
|
27
|
+
if (!chip) return nothing;
|
|
28
|
+
return html`<span class="model tools-chip" title=${chip.title}>${chip.text}</span>`;
|
|
29
|
+
}
|
|
@@ -197,9 +197,14 @@ export function llmContextNotice(data) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// The one muted line for a turn that ended with no answer (turn_completed's
|
|
200
|
-
// turn_summary.empty_answer, or a reloaded turn's marker)
|
|
201
|
-
//
|
|
202
|
-
|
|
200
|
+
// turn_summary.empty_answer, or a reloaded turn's marker), or one the
|
|
201
|
+
// provider refused (its `refusal`, with the category when named): the TUI
|
|
202
|
+
// words it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
|
|
203
|
+
export function emptyAnswerLine({ retries = 0, refusal = null } = {}) {
|
|
204
|
+
if (refusal) {
|
|
205
|
+
const category = String(refusal.category ?? "").trim();
|
|
206
|
+
return `no answer: refused by the provider${category ? ` (${category})` : ""}`;
|
|
207
|
+
}
|
|
203
208
|
const count = Number(retries) || 0;
|
|
204
209
|
const after = count > 0 ? ` (after ${count} ${count === 1 ? "retry" : "retries"})` : "";
|
|
205
210
|
return `no answer: the model returned nothing${after}`;
|
|
@@ -254,9 +259,11 @@ export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStar
|
|
|
254
259
|
switch (event.type) {
|
|
255
260
|
case "turn_enqueued": {
|
|
256
261
|
const id = event.enqueued_id;
|
|
257
|
-
|
|
262
|
+
// Queued for after the running turn (delivery queue): its bubble says so.
|
|
263
|
+
const delivery = event.delivery ? { delivery: event.delivery } : {};
|
|
264
|
+
if (isOwn(event.client_id, myId)) return [withImages({ op: "tag", enqueuedId: id, prompt: event.prompt, ...delivery }, event.images)];
|
|
258
265
|
if (id && known(id)) return [];
|
|
259
|
-
return [withImages({ op: "add", enqueuedId: id, prompt: event.prompt, state: "queued", label: clientLabel(event.client_id) }, event.images)];
|
|
266
|
+
return [withImages({ op: "add", enqueuedId: id, prompt: event.prompt, state: "queued", label: clientLabel(event.client_id), ...delivery }, event.images)];
|
|
260
267
|
}
|
|
261
268
|
case "turn_started": {
|
|
262
269
|
if (event.continue || !event.prompt) return [];
|
|
@@ -308,7 +315,10 @@ export function replaysDrawnTurn(event, turnRecords = []) {
|
|
|
308
315
|
export function snapshotEvents({ current_turn: turn = null, queued = [], queued_commands: commands = [], started_at = undefined } = {}) {
|
|
309
316
|
const events = [];
|
|
310
317
|
if (turn) {
|
|
311
|
-
|
|
318
|
+
// The run's cut wait (TurnAccumulator, as turn_started carried it): the
|
|
319
|
+
// composer's delivery line reads it.
|
|
320
|
+
const cutAfter = turn.cut_after != null ? { cut_after: turn.cut_after } : {};
|
|
321
|
+
events.push(withImages({ type: "turn_started", prompt: turn.prompt, origin: turn.origin, continue: !!turn.continue, started_at, ...cutAfter }, turn.images));
|
|
312
322
|
let textIteration = null; // a text bubble is open for this iteration
|
|
313
323
|
const closeText = () => {
|
|
314
324
|
if (textIteration !== null) events.push({ type: "generation_completed" });
|
|
@@ -340,7 +350,10 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
340
350
|
// EVENT_FIELDS), as the live events carry them.
|
|
341
351
|
const title = pickFields(part, ACTIVITY_FIELDS);
|
|
342
352
|
const fields = pickFields(part, EVENT_FIELDS);
|
|
343
|
-
|
|
353
|
+
// A running call's time so far (TurnAccumulator elapsed_ms): a join
|
|
354
|
+
// clocks it from its start (activity.js markStart).
|
|
355
|
+
const elapsed = part.status === "running" && part.elapsed_ms != null ? { elapsed_ms: part.elapsed_ms } : {};
|
|
356
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...fields, ...elapsed });
|
|
344
357
|
if (part.status !== "running") {
|
|
345
358
|
// The activity as the live event had it; action and duration_ms
|
|
346
359
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -385,7 +398,8 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
385
398
|
}
|
|
386
399
|
}
|
|
387
400
|
for (const entry of queued || []) {
|
|
388
|
-
|
|
401
|
+
const delivery = entry.delivery ? { delivery: entry.delivery } : {};
|
|
402
|
+
events.push(withImages({ type: "turn_enqueued", enqueued_id: entry.enqueued_id, client_id: entry.client_id, prompt: entry.prompt, ...delivery }, entry.images));
|
|
389
403
|
}
|
|
390
404
|
// Commands waiting for the turn's end: their bubbles, as live.
|
|
391
405
|
for (const entry of commands || []) {
|
|
@@ -116,8 +116,22 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
|
|
|
116
116
|
// gave it back). Any other state ("started", none) is a plain bubble.
|
|
117
117
|
export const BADGES = { queued: "queued", steered: "steered", failed: "failed" };
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// A queued line's badge by its delivery (the composer's choice, the
|
|
120
|
+
// turn_enqueued's): what it waits for. next_step: the running turn's next
|
|
121
|
+
// step; cut: the cut (the ack may say it waits); queue: a turn of its own
|
|
122
|
+
// after this one.
|
|
123
|
+
const QUEUED_BADGES = { next_step: "waits for the next step", cut: "cutting in\u2026", queue: "after this turn" };
|
|
124
|
+
|
|
125
|
+
// A bubble's badge words for +state+ (and +delivery+ while queued); null
|
|
126
|
+
// for a plain bubble.
|
|
127
|
+
export function badgeText(state, delivery = null) {
|
|
128
|
+
if (!BADGES[state]) return null;
|
|
129
|
+
return (state === "queued" && QUEUED_BADGES[delivery]) || BADGES[state];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function stateBadgeTemplate(state, delivery = null) {
|
|
133
|
+
const text = badgeText(state, delivery);
|
|
134
|
+
return text ? html`<span class="state-badge">${text}</span>` : nothing;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
// A `!cmd`'s saved user message (the worker appends "!(<command>)\n<output>"
|
|
@@ -203,10 +217,12 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
|
|
|
203
217
|
// merged mid-turn (a reload's; a resolved question goes before it).
|
|
204
218
|
// The live page's own: +own+ (this tab sent it), +enqueuedId+ and +key+
|
|
205
219
|
// (its normalized text: an echo waiting for its id is matched by it).
|
|
206
|
-
|
|
220
|
+
// +delivery+: how it was sent into a running turn (next_step, cut, queue):
|
|
221
|
+
// its data-delivery and its queued badge's words.
|
|
222
|
+
export function userBubbleTemplate(message, { label = null, state = null, step = null, thumbs = nothing, own = false, enqueuedId = null, key = null, delivery = null } = {}) {
|
|
207
223
|
const named = BADGES[state] ? state : null;
|
|
208
224
|
const from = label ? html`<span class="origin-label">${label}</span>` : nothing;
|
|
209
|
-
return html`<div class=${`bubble user${named ? ` ${named}` : ""}`} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing}>${from}<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>${stateBadgeTemplate(named)}</div>`;
|
|
225
|
+
return html`<div class=${`bubble user${named ? ` ${named}` : ""}`} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing}>${from}<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>${stateBadgeTemplate(named, delivery)}</div>`;
|
|
210
226
|
}
|
|
211
227
|
|
|
212
228
|
// The notice of a turn that ended with no answer (the server's
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// Rules:
|
|
8
8
|
// - turn_started opens the first gen (no iteration yet).
|
|
9
9
|
// - generation_started(n) opens a new gen unless the current one is still
|
|
10
|
-
// empty (then it takes n)
|
|
10
|
+
// empty (then it takes n); either way the gen's startedAt is the page's
|
|
11
|
+
// clock at it (null for a replay).
|
|
11
12
|
// - a chunk goes to the gen of its iteration (opening one when none has it);
|
|
12
13
|
// a chunk with no iteration goes to the current gen, unless that one is
|
|
13
14
|
// textDone or already has tool rows (an older server's replay), which
|
|
@@ -45,7 +46,7 @@ function isEmpty(gen) {
|
|
|
45
46
|
|
|
46
47
|
function openGen(turn, iteration) {
|
|
47
48
|
const closed = closeCurrent(turn);
|
|
48
|
-
const gen = { iteration: iteration ?? null, thinking: "", text: "", activity: newActivity(), textDone: false, closed: false, notices: [] };
|
|
49
|
+
const gen = { iteration: iteration ?? null, thinking: "", text: "", activity: newActivity(), textDone: false, closed: false, notices: [], startedAt: null, thinkingAt: null };
|
|
49
50
|
gen.tools = gen.activity.rows;
|
|
50
51
|
turn.gens.push(gen);
|
|
51
52
|
return { gen, opened: true, closed };
|
|
@@ -81,16 +82,21 @@ export function turnStarted(turn) {
|
|
|
81
82
|
return openGen(turn, null);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// +now+ (ms): when the page saw the generation start, its startedAt (the
|
|
86
|
+
// composer's delivery line clocks the step from it); null for a replayed
|
|
87
|
+
// start (a join: the snapshot's generation age stands in).
|
|
88
|
+
export function generationStarted(turn, iteration = null, now = null) {
|
|
85
89
|
const current = currentGen(turn);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return openGen(turn, iteration);
|
|
90
|
+
const r = current && isEmpty(current) ? { gen: current, opened: false, closed: null } : openGen(turn, iteration);
|
|
91
|
+
if (!r.opened) r.gen.iteration = iteration ?? r.gen.iteration;
|
|
92
|
+
r.gen.startedAt = now;
|
|
93
|
+
return r;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
// +now+ (ms): when the page got the chunk (null for a replay): a gen's
|
|
97
|
+
// first thinking delta stamps its thinkingAt, the clock the worker's cut
|
|
98
|
+
// counts from too (GenerationPhase).
|
|
99
|
+
export function chunk(turn, { text = "", thinking = "", iteration = null, now = null } = {}) {
|
|
94
100
|
let r;
|
|
95
101
|
if (iteration == null) {
|
|
96
102
|
const current = currentGen(turn);
|
|
@@ -102,6 +108,7 @@ export function chunk(turn, { text = "", thinking = "", iteration = null } = {})
|
|
|
102
108
|
// Leading whitespace (the "\n" after <think>, or after a closed
|
|
103
109
|
// think/tool_call block) would render as an empty first line; trim it at
|
|
104
110
|
// the start of a gen's thinking and text only.
|
|
111
|
+
if (thinking && !gen.thinking && now != null) gen.thinkingAt = now;
|
|
105
112
|
if (thinking) gen.thinking += leadTrimmed(gen.thinking, thinking);
|
|
106
113
|
if (text) gen.text += leadTrimmed(gen.text, text);
|
|
107
114
|
return r;
|
|
@@ -700,14 +700,16 @@ export function createTurnView({
|
|
|
700
700
|
draw(entry);
|
|
701
701
|
}
|
|
702
702
|
},
|
|
703
|
-
|
|
703
|
+
// +now+: the page's clock at a live start (stream_handlers.js), null
|
|
704
|
+
// for a replayed one.
|
|
705
|
+
generationStarted(data, now = null) {
|
|
704
706
|
if (!turn) this.turnStarted({});
|
|
705
|
-
apply(model.generationStarted(turn, data.iteration ?? null));
|
|
707
|
+
apply(model.generationStarted(turn, data.iteration ?? null, now));
|
|
706
708
|
},
|
|
707
|
-
chunk({ text, thinking, iteration }) {
|
|
709
|
+
chunk({ text, thinking, iteration, now = null }) {
|
|
708
710
|
if (!text && !thinking) return;
|
|
709
711
|
if (!turn) this.turnStarted({});
|
|
710
|
-
const r = model.chunk(turn, { text, thinking, iteration });
|
|
712
|
+
const r = model.chunk(turn, { text, thinking, iteration, now });
|
|
711
713
|
apply(r);
|
|
712
714
|
scheduleFlush(r.gen);
|
|
713
715
|
},
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -878,6 +878,8 @@ module Samagotchi
|
|
|
878
878
|
# /reasoning-history or /model: the level the next turn runs under
|
|
879
879
|
# (the web's chip); nil on a native host, which ignores it.
|
|
880
880
|
event[:reasoning_history] = @engine.reasoning_history_explained&.summary if changed.intersect?(%i[reasoning_history model])
|
|
881
|
+
# /toolkit: what the next turn declares (the web's chip; nil on full).
|
|
882
|
+
event[:toolkit] = @engine.toolkit_summary if changed.include?(:toolkit)
|
|
881
883
|
event[:confirm] = confirm.to_h if confirm
|
|
882
884
|
event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
|
|
883
885
|
@engine.announce(event)
|