samagotchi 0.61.0 → 0.62.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 +59 -1
- data/docs/architecture.md +3 -0
- data/docs/cli.md +22 -8
- data/docs/configuration.md +32 -19
- data/docs/context.md +8 -0
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +8 -0
- data/docs/internals/engine.md +24 -11
- data/docs/internals/prompt-caching.md +6 -1
- data/docs/plugins.md +42 -3
- data/docs/sessions.md +4 -4
- data/docs/testing.md +4 -1
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -1
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +58 -3
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -4
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +9 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/kernel_loop.rb +7 -16
- data/lib/samagotchi/llm/chat_loop.rb +23 -15
- data/lib/samagotchi/plugin/api.rb +36 -18
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/row_refs.rb +126 -0
- data/lib/samagotchi/session_metrics.rb +24 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +26 -3
- data/lib/samagotchi/tool_activity.rb +8 -4
- data/lib/samagotchi/tool_response.rb +5 -4
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +16 -3
- data/lib/samagotchi/tools/builtins.rb +9 -2
- data/lib/samagotchi/tools/execute.rb +15 -3
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/registry.rb +5 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -1
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +4 -1
- data/lib/samagotchi/web/public/app.js +59 -15
- data/lib/samagotchi/web/public/chains.js +18 -20
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/index.html +61 -10
- data/lib/samagotchi/web/public/keep_focus.js +24 -0
- data/lib/samagotchi/web/public/lists_view.js +13 -13
- data/lib/samagotchi/web/public/llm_context_chip.js +55 -34
- data/lib/samagotchi/web/public/question_card.js +11 -0
- data/lib/samagotchi/web/public/question_cards.js +60 -5
- data/lib/samagotchi/web/public/ref_menu.js +18 -5
- data/lib/samagotchi/web/public/refs.js +113 -15
- data/lib/samagotchi/web/public/scroll.js +42 -0
- data/lib/samagotchi/web/public/stage_model.js +22 -3
- data/lib/samagotchi/web/public/stage_view.js +299 -70
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +5 -4
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +104 -10
- data/lib/samagotchi/worker.rb +3 -2
- data/lib/samagotchi/worker_inbound.rb +5 -3
- metadata +3 -1
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
// A parent's card with its delegates folded in (docs/sessions.md, the web):
|
|
2
|
-
// the chip that opens the family's list, and the list's rows
|
|
3
|
-
//
|
|
4
|
-
// strip) and binds the clicks.
|
|
5
|
-
|
|
2
|
+
// the chip that opens the family's list, and the list's rows, as lit-html
|
|
3
|
+
// templates; lists_view.js places them (inline in all sessions, a popover
|
|
4
|
+
// over the strip) and binds the clicks. Attributes after a bound one are
|
|
5
|
+
// bound too, so they keep their order in the markup (lit sets bound ones
|
|
6
|
+
// after the static ones).
|
|
7
|
+
import { ownerBadge, previewForCard, relativeTime } from "./format.js";
|
|
6
8
|
import { familySummary, stoppedBadge, waitingBadge } from "./sessions_list.js";
|
|
9
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
10
|
+
import { keepFocus } from "./keep_focus.js";
|
|
7
11
|
|
|
8
12
|
// The chip in a parent's meta row: "▸ 3 delegates · ● 1 live · 1 waiting"
|
|
9
13
|
// (a waiting one in the attention colour), its tooltip the delegates by
|
|
10
|
-
// state.
|
|
14
|
+
// state. nothing for a family with no members.
|
|
11
15
|
// @param open whether the list is open (aria-expanded; the arrow turns)
|
|
12
|
-
export function
|
|
16
|
+
export function familyChipTemplate(f, { open = false } = {}) {
|
|
13
17
|
const sum = familySummary(f);
|
|
14
|
-
if (!sum) return
|
|
18
|
+
if (!sum) return nothing;
|
|
15
19
|
const waiting = sum.counts.waiting;
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
return `<button type="button" class
|
|
20
|
+
const live = sum.live ? html`<span class="fc-live"> · <span class="live-dot" aria-hidden="true"></span>${sum.live} live</span>` : nothing;
|
|
21
|
+
const waitingT = waiting ? html` · <span class="fc-waiting">${waiting} waiting</span>` : nothing;
|
|
22
|
+
return html`<button type="button" class=${`family-chip${waiting ? " waiting" : ""}`} aria-expanded=${open ? "true" : "false"}
|
|
23
|
+
title=${sum.title}><span class="fc-arrow" aria-hidden="true">▸</span>${sum.total} ${sum.total === 1 ? "delegate" : "delegates"}${
|
|
24
|
+
live}${waitingT}</button>`;
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
// A row's status dot (a family's or a chain's): the waiting title over the
|
|
28
|
+
// status when the session waits on the user.
|
|
29
|
+
export function rowDotTemplate(s, waiting) {
|
|
30
|
+
const word = (waiting ? waiting.title : s.status) || "";
|
|
31
|
+
return html`<span class=${`status dot ${s.status || ""}${waiting ? " waiting" : ""}`} title=${word} aria-label=${word}></span>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A row's waiting or stopped badge, and the archived one.
|
|
35
|
+
export function rowBadgesTemplate(waiting, stopped) {
|
|
36
|
+
return html`${waiting ? html`<span class=${`attn attn-${waiting.kind}`} title=${waiting.title}>${waiting.text}</span>` : nothing}${
|
|
37
|
+
stopped ? html`<span class="stopped-badge" title=${stopped.title}>${stopped.text}</span>` : nothing}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const archivedBadge = html`<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>`;
|
|
41
|
+
|
|
21
42
|
// The family's list: one row per member, depth first, indented one step
|
|
22
43
|
// per depth (--depth): status dot, preview, time, the waiting / stopped
|
|
23
44
|
// badge, the owner badge, archived. A row's click opens that session.
|
|
@@ -25,7 +46,7 @@ export function familyChipHtml(f, { open = false } = {}) {
|
|
|
25
46
|
// @param matchIds a search's matching members (.match; the rest .dim), or
|
|
26
47
|
// null without a search
|
|
27
48
|
// @param archivable an archive button per row (all sessions, not in select mode)
|
|
28
|
-
export function
|
|
49
|
+
export function familyRowsTemplate(f, { activeId = null, matchIds = null, archivable = false, now = Date.now() } = {}) {
|
|
29
50
|
const rows = f.members.map(({ session: s, depth }) => {
|
|
30
51
|
const cls = ["family-row"];
|
|
31
52
|
if (activeId && s.id === activeId) cls.push("active");
|
|
@@ -36,23 +57,17 @@ export function familyRowsHtml(f, { activeId = null, matchIds = null, archivable
|
|
|
36
57
|
const stopped = waiting ? null : stoppedBadge(s);
|
|
37
58
|
const owner = ownerBadge(s.owner, s.id);
|
|
38
59
|
const ago = relativeTime(ts, now);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
`<
|
|
42
|
-
`<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
ago ? `<span class="time">${
|
|
46
|
-
owner ? `<span class
|
|
47
|
-
s.archived ?
|
|
48
|
-
].join("");
|
|
49
|
-
const short = escapeHtml(s.short_id || String(s.id).slice(0, 8));
|
|
50
|
-
const arch = !archivable ? "" : s.archived
|
|
51
|
-
? `<button type="button" class="card-arch" data-arch="${escapeHtml(s.id)}" title="Unarchive: back to the lists" aria-label="Unarchive session ${short}"></button>`
|
|
52
|
-
: `<button type="button" class="card-arch" data-arch="${escapeHtml(s.id)}" title="Archive: hide from the lists, keep for good" aria-label="Archive session ${short}"></button>`;
|
|
53
|
-
return `<div class="${cls.join(" ")}" data-id="${escapeHtml(s.id)}" style="--depth:${depth}" role="listitem"><button type="button" class="fr-open" title="${escapeHtml(`${s.id} · ${s.status || ""}`)}">${parts}</button>${arch}</div>`;
|
|
60
|
+
const short = s.short_id || String(s.id).slice(0, 8);
|
|
61
|
+
const arch = !archivable ? nothing : s.archived
|
|
62
|
+
? html`<button type="button" class="card-arch" data-arch=${s.id} title=${"Unarchive: back to the lists"} aria-label=${`Unarchive session ${short}`}></button>`
|
|
63
|
+
: html`<button type="button" class="card-arch" data-arch=${s.id} title=${"Archive: hide from the lists, keep for good"} aria-label=${`Archive session ${short}`}></button>`;
|
|
64
|
+
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${`--depth:${depth}`} role=${"listitem"}><button type="button" class="fr-open"
|
|
65
|
+
title=${`${s.id} · ${s.status || ""}`}>${rowDotTemplate(s, waiting)}<span class="fr-preview">${previewForCard(s)}</span>${
|
|
66
|
+
rowBadgesTemplate(waiting, stopped)}${ago ? html`<span class="time">${ago}</span>` : nothing}${
|
|
67
|
+
owner ? html`<span class=${`owner ${owner.kind}`} title=${owner.title}>${owner.text}</span>` : nothing}${
|
|
68
|
+
s.archived ? archivedBadge : nothing}</button>${arch}</div>`;
|
|
54
69
|
});
|
|
55
|
-
return `<div class="family-rows" role="list">${rows
|
|
70
|
+
return html`<div class="family-rows" role="list">${rows}</div>`;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
// The widest window index.html lays out as a phone (@media (max-width:600px)).
|
|
@@ -61,11 +76,13 @@ export const PHONE_MAX_PX = 600;
|
|
|
61
76
|
// The strip's family popover: one #familyPop in body (position: fixed, so
|
|
62
77
|
// the strip's sideways scroll on a phone doesn't clip it), under the chip
|
|
63
78
|
// it was opened from and as wide as that card (300 px at least, inside the
|
|
64
|
-
// window; on a phone, the window less 16 px gutters). The strip draws again
|
|
65
|
-
// the new chip for the same head,
|
|
79
|
+
// window; on a phone, the window less 16 px gutters). The strip draws again
|
|
80
|
+
// on every hub event: refresh() then finds the new chip for the same head,
|
|
81
|
+
// draws the rows again (lit writes only what changed: a row keeps its
|
|
82
|
+
// nodes, reused by position, so keepFocus keeps the focus on its session)
|
|
66
83
|
// and closes when the family is gone.
|
|
67
|
-
// @param rowsFor (headId) → the rows'
|
|
68
|
-
// members (any more)
|
|
84
|
+
// @param rowsFor (headId) → the rows' template, or null when the family
|
|
85
|
+
// has no members (any more)
|
|
69
86
|
// @param anchorFor (headId) → its chip in the strip, or null
|
|
70
87
|
// @param onPick (id) a row's click
|
|
71
88
|
// @param onClose () after it closed (the strip's held order catches up)
|
|
@@ -73,7 +90,6 @@ export const PHONE_MAX_PX = 600;
|
|
|
73
90
|
export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor, onPick, onClose = () => {} }) {
|
|
74
91
|
let el = null;
|
|
75
92
|
let headId = null;
|
|
76
|
-
let html = "";
|
|
77
93
|
|
|
78
94
|
const element = () => {
|
|
79
95
|
if (el) return el;
|
|
@@ -110,10 +126,8 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
110
126
|
close();
|
|
111
127
|
return false;
|
|
112
128
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
html = rows;
|
|
116
|
-
}
|
|
129
|
+
// A row inserted before the focused one: the focus stays on its session.
|
|
130
|
+
keepFocus(el, "data-id", () => render(rows, el));
|
|
117
131
|
anchor.setAttribute("aria-expanded", "true");
|
|
118
132
|
place(anchor);
|
|
119
133
|
return true;
|
|
@@ -123,7 +137,6 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
123
137
|
if (headId && headId !== id) close();
|
|
124
138
|
headId = id;
|
|
125
139
|
element().hidden = false;
|
|
126
|
-
html = "";
|
|
127
140
|
draw();
|
|
128
141
|
}
|
|
129
142
|
|
|
@@ -131,9 +144,8 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
131
144
|
if (!headId) return;
|
|
132
145
|
anchorFor(headId)?.setAttribute("aria-expanded", "false");
|
|
133
146
|
headId = null;
|
|
134
|
-
html = "";
|
|
135
147
|
el.hidden = true;
|
|
136
|
-
el
|
|
148
|
+
render(nothing, el);
|
|
137
149
|
onClose();
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -283,6 +283,15 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
283
283
|
.gen details.thinking summary.thinking-fade{animation:thinking-fade 120ms ease-out}
|
|
284
284
|
@keyframes thinking-fade{from{opacity:.4}to{opacity:1}}
|
|
285
285
|
@media (prefers-reduced-motion:reduce){.gen details.thinking summary.thinking-fade{animation:none}}
|
|
286
|
+
/* A finished block's outline (turn_view.js outlineBlock): open on its narrated steps' rows; a run of steps that said
|
|
287
|
+
nothing is one "· N steps" fold, a row like a step's, that shows them. */
|
|
288
|
+
.turn-work.outline{--step-indent:14px}
|
|
289
|
+
.turn-work.outline>.gen.folded:not(.unfolded){display:none}
|
|
290
|
+
.outline-fold{margin-top:8px;padding-top:6px;padding-left:var(--step-indent);border-top:1px solid var(--border);font-size:12px;font-weight:500;color:var(--text-faint);cursor:pointer;user-select:none;border-radius:4px}
|
|
291
|
+
.outline-fold::before{content:"· "}
|
|
292
|
+
.outline-fold.open::before{content:"▾ "}
|
|
293
|
+
.outline-fold:hover{color:var(--text-muted)}
|
|
294
|
+
.turn-work.outline>.gen.folded.unfolded{margin-left:var(--step-indent)}
|
|
286
295
|
.gen-text{margin-top:6px;white-space:pre-wrap;word-break:break-word;font-size:15px;line-height:1.55}
|
|
287
296
|
/* The live step's narration box: the complete sentences so far, one per line, three visual lines high (3 × the 1.55 line-height), scrolled to the newest; each new line fades in. */
|
|
288
297
|
.gen-text.streaming>.narration{max-height:4.65em;overflow:auto;scrollbar-width:thin;white-space:normal}
|
|
@@ -310,6 +319,12 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
310
319
|
.bubble.question>summary::-webkit-details-marker{display:none}
|
|
311
320
|
.bubble.question>summary::before{content:"▸ ";color:var(--warn)}
|
|
312
321
|
.bubble.question[open]>summary::before{content:"▾ "}
|
|
322
|
+
/* An answered card keeps the reply that asked it (question_cards.js): muted under its "Q → A" line, two lines while
|
|
323
|
+
closed (newlines as spaces); open, the summary's copy goes and the body's starts the card, whole, as written and
|
|
324
|
+
selectable. */
|
|
325
|
+
.bubble.question>summary .question-said{max-width:72ch;margin:2px 0 1px 1.05em;font-size:12.5px;font-weight:400;line-height:1.5;color:var(--text-muted);white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
|
|
326
|
+
.bubble.question[open]>summary .question-said{display:none}
|
|
327
|
+
.question-said-full{margin:4px 0 6px;font-size:13px;line-height:1.55;color:var(--text-muted);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
313
328
|
.bubble.command{border-left:3px solid var(--text-faint);background:var(--bg-panel);font-size:13px}
|
|
314
329
|
.bubble.note{border-left:3px dotted var(--text-faint);background:transparent;font-size:12px;color:var(--text-muted);opacity:.85}
|
|
315
330
|
.note-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);margin-bottom:2px}
|
|
@@ -336,7 +351,8 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
336
351
|
.bubble.question.answered{border-left-color:var(--ok)}
|
|
337
352
|
.bubble.question.cancelled{border-left-color:var(--text-muted);opacity:.75}
|
|
338
353
|
/* A settled card stops floating: the highlight stays, the drop shadow goes. */
|
|
339
|
-
|
|
354
|
+
/* Closed, it is a bare line (no card to light): wider with its kept reply, a highlight would read as a stray rule. */
|
|
355
|
+
.bubble.question.answered[open],.bubble.question.cancelled[open]{box-shadow:inset 0 1px 0 var(--glass-highlight)}
|
|
340
356
|
.question-text{font-size:14px;color:var(--text);margin-bottom:8px;white-space:pre-wrap}
|
|
341
357
|
.question-options{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}
|
|
342
358
|
.question-option{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--text-2);padding:6px 8px;border:1px solid var(--border);border-radius:8px;cursor:pointer}
|
|
@@ -394,6 +410,8 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
394
410
|
.ref-more[hidden]{display:none}
|
|
395
411
|
/* A ref makes room after itself for its ▾ while it shows (ref_menu.js), and keeps its hover look (its ↗ too) until the ▾ goes: the ▾ never covers what follows it. */
|
|
396
412
|
.ref[data-ref-more]{padding-right:19px}
|
|
413
|
+
/* …unless the room would push it onto the next line: it ends its line, and the ▾ goes past its end. */
|
|
414
|
+
.ref[data-ref-more="end"]{padding-right:0}
|
|
397
415
|
.ref-more:hover{color:var(--accent);border-color:var(--accent)}
|
|
398
416
|
.ref-menu{position:fixed;z-index:61;min-width:160px;max-width:calc(100vw - 16px);padding:4px;border:1px solid var(--border-strong);border-radius:8px;background:var(--bg-panel);box-shadow:var(--toast-shadow);animation:fadeIn .12s ease-out}
|
|
399
417
|
.ref-menu [role="menuitem"]{display:block;width:100%;padding:6px 10px;border:0;border-radius:5px;background:none;color:var(--text);font:inherit;font-size:13px;text-align:left;cursor:pointer;white-space:nowrap}
|
|
@@ -594,7 +612,13 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
594
612
|
body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;border:0;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;gap:10px}
|
|
595
613
|
.dock-card{display:flex;flex-direction:column;border-radius:16px;position:relative;transition:border-color .12s ease,box-shadow .12s ease}
|
|
596
614
|
.dock-card:focus-within{border-color:color-mix(in srgb,var(--accent) 70%,transparent);box-shadow:0 0 0 3px var(--accent-ring),var(--glass-shadow)}
|
|
615
|
+
/* The live slots' heights (headline, tool row, trail); --ts-live-h is their running height with the two 8 px gaps. */
|
|
616
|
+
.turn-stage{--ts-head-h:42px;--ts-now-h:32px;--ts-trail-h:18px;--ts-live-h:calc(var(--ts-head-h) + var(--ts-now-h) + var(--ts-trail-h) + 16px)}
|
|
597
617
|
.turn-stage{display:flex;flex-direction:column;min-height:0;max-height:var(--stage-max,60vh);border-left:3px solid var(--accent);border-radius:14px;overflow:hidden}
|
|
618
|
+
/* Out of the dock's flow: it floats 10 px above the composer card, so its rise, growth and leaving never move #dock (a
|
|
619
|
+
bottom-pinned box changing height is a layout shift, animated or not). Painted before .dock-card (no z-index): the command
|
|
620
|
+
list (z-index 20 inside .dock-card) opens over its lower edge. The history's bottom reserve counts it (app.js, --dock-h). */
|
|
621
|
+
body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-bottom:10px}
|
|
598
622
|
.turn-stage[hidden]{display:none}
|
|
599
623
|
.turn-stage.ended{border-left-color:var(--border-strong)}
|
|
600
624
|
.turn-stage[data-phase="answered"]{border-left-color:var(--ok)}
|
|
@@ -603,7 +627,10 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
603
627
|
.turn-stage[data-phase="no answer"]{border-left-color:var(--warn)}
|
|
604
628
|
.turn-stage.enter{animation:ts-rise .35s cubic-bezier(.3,.7,.2,1)}
|
|
605
629
|
@keyframes ts-rise{from{opacity:0;transform:translateY(14px)}}
|
|
606
|
-
|
|
630
|
+
/* The hand-off: the stage slides away under the composer still drawn (a transform: nothing moves; stage_view.js handOff),
|
|
631
|
+
then the turn arrives in the history. Meanwhile the history's bottom reserve eases down (app.js). */
|
|
632
|
+
.turn-stage.leaving{transition:transform .42s cubic-bezier(.4,0,.6,1),opacity .42s ease-in;transform:translateY(40px);opacity:0;pointer-events:none}
|
|
633
|
+
#center.reserve-easing #history{transition:padding-bottom .42s cubic-bezier(.4,0,.6,1)}
|
|
607
634
|
.ts-arrive{animation:ts-arrive .5s cubic-bezier(.3,.7,.2,1)}
|
|
608
635
|
@keyframes ts-arrive{from{opacity:0;transform:translateY(60px)}}
|
|
609
636
|
.ts-status{flex:none;display:flex;align-items:center;gap:8px;min-width:0;padding:8px 8px 0 14px;font-size:12px;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
@@ -640,20 +667,24 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
640
667
|
.ts-prompt:not(.full) .user-message blockquote{display:inline;border:0;padding:0;margin:0}
|
|
641
668
|
/* The live slots: fixed heights (two headline lines, one tool row, one trail line). */
|
|
642
669
|
.ts-live{display:flex;flex-direction:column;gap:8px}
|
|
643
|
-
.ts-headline{height:
|
|
670
|
+
.ts-headline{height:var(--ts-head-h);font-size:14px;line-height:21px;color:var(--narr);overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;cursor:pointer;border-radius:6px}
|
|
644
671
|
.ts-headline:hover{background:var(--bg-hover)}
|
|
645
672
|
.ts-headline:not(.thinking)::before{content:"\203A";color:var(--accent);font-weight:700;margin-right:7px}
|
|
646
673
|
.ts-headline.thinking{font-size:13.5px;font-style:italic;color:var(--text-muted)}
|
|
647
674
|
.ts-think-tag{font-style:normal;font-size:12.5px;color:var(--think-text);margin-right:6px}
|
|
648
675
|
.ts-headline.fresh{animation:ts-fade .3s ease}
|
|
676
|
+
/* While a question or approval waits: the whole reply that asked it, raw as a closed step's text, in the headline's
|
|
677
|
+
place; capped so the card keeps its room, read from the top (stage_view.js drawAsking). */
|
|
678
|
+
.ts-asking{max-height:calc(var(--stage-max,60vh) * .35);overflow:auto;scrollbar-width:thin;white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.55;color:var(--narr);border-left:2px solid var(--border-strong);padding-left:10px}
|
|
679
|
+
.turn-stage.asking .ts-headline{display:none}
|
|
649
680
|
@keyframes ts-fade{from{opacity:.35}}
|
|
650
|
-
.ts-now{height:
|
|
681
|
+
.ts-now{height:var(--ts-now-h);display:flex;align-items:center;gap:8px;min-width:0;padding:0 9px;border-radius:8px;background:var(--accent-tint);font-size:13px}
|
|
651
682
|
.ts-spin{flex:none;width:11px;height:11px;border:2px solid var(--warn);border-right-color:transparent;border-radius:50%;animation:init-spin .8s linear infinite}
|
|
652
683
|
.ts-idle{flex:none;width:11px;height:11px;border:2px solid var(--border-strong);border-radius:50%}
|
|
653
684
|
.ts-tool{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:600;color:var(--accent)}
|
|
654
685
|
.ts-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--text-2)}
|
|
655
686
|
.ts-title.quiet{font-family:inherit;font-style:italic;color:var(--text-faint)}
|
|
656
|
-
.ts-trail{height:
|
|
687
|
+
.ts-trail{height:var(--ts-trail-h);display:flex;gap:10px;overflow:hidden;font-size:12px;line-height:18px;color:var(--text-faint);white-space:nowrap}
|
|
657
688
|
.ts-trail-item{flex:none;max-width:36ch;overflow:hidden;text-overflow:ellipsis}
|
|
658
689
|
.ts-trail-item:is(.error,.blocked){color:var(--danger-text)}
|
|
659
690
|
.ts-trail-item.warn{color:var(--warn-text)}
|
|
@@ -664,7 +695,14 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
664
695
|
.turn-stage.waiting .ts-now,.turn-stage.waiting .ts-trail{display:none}
|
|
665
696
|
/* Rows that arrive during the turn (cards, queued prompts, commands, notes) and after its end (the end line): capped, own scroll. */
|
|
666
697
|
.ts-extras,.ts-tail{display:flex;flex-direction:column;gap:8px;max-height:calc(var(--stage-max,60vh) * .4);overflow:auto;scrollbar-width:thin}
|
|
667
|
-
.turn-stage.
|
|
698
|
+
.turn-stage.held-wait .ts-extras{max-height:calc(var(--stage-max,60vh) * .7)}
|
|
699
|
+
/* The card under the live slots stays put from its arrival to the next step: waiting, the slots keep at least their running
|
|
700
|
+
height (the asking text in the headline's place, the tool row's and the trail's room left blank); answered, the height
|
|
701
|
+
they had while waiting (stage_view.js). */
|
|
702
|
+
.turn-stage.waiting .ts-live{min-height:var(--ts-live-h)}
|
|
703
|
+
.turn-stage.held-wait:not(.waiting) .ts-live{min-height:max(var(--ts-live-h),var(--live-held,0px))}
|
|
704
|
+
/* A 1 px mark at the tallest content's end, so the scroll extent never drops while a turn runs (stage_view.js fitFrame). */
|
|
705
|
+
.ts-hold{position:absolute;left:0;top:0;width:1px;height:1px;visibility:hidden;pointer-events:none}
|
|
668
706
|
.ts-extras>*,.ts-tail>*{flex-shrink:0}
|
|
669
707
|
.ts-extras>.bubble.user{max-width:none;margin:0;padding:4px 10px;font-size:13px;line-height:1.45;background:transparent;color:var(--text-2)}
|
|
670
708
|
.ts-extras>.bubble.user>.copy-btn{display:none}
|
|
@@ -678,12 +716,14 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
678
716
|
.ts-answer>.bubble.output{border-left:0;background:transparent;border:0;padding:0}
|
|
679
717
|
.ts-answer>.bubble.output>.copy-btn{top:0;right:0}
|
|
680
718
|
/* The cloud: one chip (steps, calls, a tick per call), the turn view's block behind it. */
|
|
681
|
-
.
|
|
719
|
+
/* The footer: the chip at the stage's bottom edge (the cloud's steps open above it, the scroller's last slot). */
|
|
720
|
+
.ts-foot{flex:none;padding:0 14px 10px}
|
|
721
|
+
.ts-foot[hidden],.turn-stage.collapsed .ts-foot{display:none}
|
|
682
722
|
.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}
|
|
683
723
|
.ts-chip:hover:not(:disabled){filter:none;background:var(--bg-hover)}
|
|
684
724
|
.ts-chip-label{font-size:12.5px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
685
725
|
.ts-chip-label::before{content:"\25B8 ";color:var(--accent)}
|
|
686
|
-
.ts-
|
|
726
|
+
.ts-foot.open .ts-chip-label::before{content:"\25BE "}
|
|
687
727
|
/* One row whatever the count (the stage's height stays): the newest ticks show, the oldest are cut on the left. */
|
|
688
728
|
.ts-ticks{display:flex;flex-wrap:nowrap;justify-content:flex-end;overflow:hidden;gap:2px;flex:0 1 auto;min-width:0}
|
|
689
729
|
.ts-tick{flex:none}
|
|
@@ -702,14 +742,18 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
702
742
|
.ts-cloud-body[hidden]{display:none}
|
|
703
743
|
.turn-stage .turn-work{border:0;background:transparent;padding:0;border-radius:0}
|
|
704
744
|
.turn-stage .turn-work>summary{display:none}
|
|
745
|
+
/* The ended turn's cloud shows every step: no folds there. */
|
|
746
|
+
.turn-stage .outline-fold{display:none}
|
|
747
|
+
.turn-stage .turn-work.outline>.gen.folded{display:block}
|
|
705
748
|
.turn-stage .ts-cloud-body .gen:first-of-type{margin-top:0;border-top:0}
|
|
706
749
|
@media(max-width:600px){
|
|
707
750
|
.ts-status{padding-left:10px}
|
|
708
751
|
.ts-scroll{padding:8px 10px 10px}
|
|
752
|
+
.ts-foot{padding:0 10px 10px}
|
|
709
753
|
.ts-defer{display:none}
|
|
710
754
|
.ts-trail-item{max-width:24ch}
|
|
711
755
|
}
|
|
712
|
-
@media (prefers-reduced-motion:reduce){.turn-stage.enter,.ts-arrive,.ts-headline.fresh,.ts-tick.running,.ts-mark,.turn-work:not(.done) .activity-status.running::before{animation:none}.turn-stage.leaving{transition:none}}
|
|
756
|
+
@media (prefers-reduced-motion:reduce){.turn-stage.enter,.ts-arrive,.ts-headline.fresh,.ts-tick.running,.ts-mark,.turn-work:not(.done) .activity-status.running::before{animation:none}.turn-stage.leaving,#center.reserve-easing #history{transition:none}}
|
|
713
757
|
#infoBar{display:flex;padding:8px 14px 10px;border-top:1px solid var(--glass-border);background:transparent;border-radius:0 0 16px 16px;color:var(--text-2);gap:10px;align-items:flex-start;flex-wrap:wrap;flex-shrink:0;transition:opacity 280ms ease}
|
|
714
758
|
#infoBar.visible{display:flex}
|
|
715
759
|
#infoBar .info{flex:1;min-width:0;font-size:13px;line-height:1.4;word-break:break-word;display:flex;flex-direction:column;gap:2px}
|
|
@@ -863,10 +907,17 @@ body.has-session #scope .scope-where{display:none}
|
|
|
863
907
|
/* The open session's title, when no strip card in sight shows it (session_title.js): the bar's middle, cut with an ellipsis; the whole first message in its tooltip. */
|
|
864
908
|
#sessionTitle{flex:1;min-width:0;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
865
909
|
#sessionTitle[hidden]{display:none}
|
|
866
|
-
/* A phone: the title is its own row under the bar, so "chi + · pill · bell" stay one line.
|
|
910
|
+
/* A phone: the title is its own row under the bar, so "chi + · pill · bell" stay one line. Nothing on that line wraps to a
|
|
911
|
+
row of its own when the pill's text grows mid-turn ("· 1 waiting"): the scope chip and the pill have a base size of 0 (what
|
|
912
|
+
a wrapping line counts) and grow to their text, the pill first; the scope chip cuts with an ellipsis before the pill does.
|
|
913
|
+
The pill drops its session count while it says live or waiting, so the scope chip keeps its words. */
|
|
867
914
|
@media(max-width:600px){
|
|
868
915
|
#topBar{flex-wrap:wrap;row-gap:2px}
|
|
869
916
|
#sessionTitle{flex-basis:100%;order:10;font-size:12px}
|
|
917
|
+
#scope,#stripShow{flex:1 1 0;min-width:0;max-width:max-content}
|
|
918
|
+
#stripShow{flex-grow:1000;overflow:hidden;text-overflow:ellipsis}
|
|
919
|
+
#scope .scope-name{flex-shrink:1;min-width:0}
|
|
920
|
+
#stripShow:has(.live,.waiting) .pill-count{display:none}
|
|
870
921
|
}
|
|
871
922
|
#notifyBtn{margin-left:auto;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;min-height:0;background:transparent;border:1px solid transparent;border-radius:999px;color:var(--text-muted)}
|
|
872
923
|
#notifyBtn[hidden]{display:none}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// lit reuses a list's nodes by position (vendor/lit-html.js has no keyed
|
|
2
|
+
// repeat): after an item is inserted before the focused one, the focused
|
|
3
|
+
// node holds another item. keepFocus draws with +draw+ and puts the focus
|
|
4
|
+
// back on the item it was on, by its +attr+ (data-id, data-name): the same
|
|
5
|
+
// node inside it (by class), the item itself else; blurred when the item is
|
|
6
|
+
// gone, so no other item keeps it. Nothing is done when the focus wasn't
|
|
7
|
+
// inside +root+.
|
|
8
|
+
export function keepFocus(root, attr, draw) {
|
|
9
|
+
const doc = root.ownerDocument;
|
|
10
|
+
const itemOf = (node) => (node && node !== root && root.contains(node) ? node.closest(`[${attr}]`) : null);
|
|
11
|
+
const was = doc.activeElement;
|
|
12
|
+
const item = itemOf(was);
|
|
13
|
+
const key = item && root.contains(item) ? item.getAttribute(attr) : null;
|
|
14
|
+
const inner = item && was !== item ? was.className : null;
|
|
15
|
+
draw();
|
|
16
|
+
if (key === null || itemOf(doc.activeElement)?.getAttribute(attr) === key) return;
|
|
17
|
+
const target = [...root.querySelectorAll(`[${attr}]`)].find((el) => el.getAttribute(attr) === key);
|
|
18
|
+
if (!target) {
|
|
19
|
+
if (root.contains(doc.activeElement)) doc.activeElement.blur?.();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const same = inner && [...target.querySelectorAll("*")].find((el) => el.className === inner);
|
|
23
|
+
(same || target).focus();
|
|
24
|
+
}
|
|
@@ -24,14 +24,13 @@
|
|
|
24
24
|
// setStripHidden(hidden)
|
|
25
25
|
|
|
26
26
|
import { ownerBadge, relativeTime, delegatedBy } from "./format.js";
|
|
27
|
-
import { htmlElement } from "./turn_html.js";
|
|
28
27
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
29
28
|
import { cardCtxText, ctxBarTip } from "./ctx.js";
|
|
30
29
|
import { allScopeHref, cardFolder } from "./scope.js";
|
|
31
30
|
import { sessionHash } from "./route.js";
|
|
32
31
|
import { stripColumns, stripShowParts } from "./strip.js";
|
|
33
|
-
import { createFamilyPop,
|
|
34
|
-
import { allViewChains,
|
|
32
|
+
import { createFamilyPop, familyChipTemplate, familyRowsTemplate } from "./families_view.js";
|
|
33
|
+
import { allViewChains, chainChipTemplate, chainRowsTemplate, chainTitle, foldChains } from "./chains.js";
|
|
35
34
|
import {
|
|
36
35
|
batchCounts, batchTargets, batchToastText, families, familyId, familyOpen, familyWaits, filterFamilies, heldOrder,
|
|
37
36
|
listedSessions, rangeIds, runBatch, stoppedBadge, waitingBadge, waitingFirst, waitingSearchText,
|
|
@@ -122,11 +121,10 @@ export function cardTemplate(s, { deletable = false, archivable = false, selecta
|
|
|
122
121
|
const pick = selectable ? html`<input type="checkbox" class="card-pick" aria-label=${`Select session ${s.short_id}`} ?checked=${picked}>` : nothing;
|
|
123
122
|
const skip = skipReason ? html`<div class="skip-reason">skipped: ${skipReason}</div>` : nothing;
|
|
124
123
|
const familyWaiting = !!family && familyWaits(family);
|
|
125
|
-
// The family and chain chips and rows
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const famRows = fam && rows && open ? htmlElement(familyRowsHtml(fam, { activeId, matchIds, archivable: !selectable })) : nothing;
|
|
124
|
+
// The family and chain chips and rows (families_view.js, chains.js).
|
|
125
|
+
const chain = family?.chain ? chainChipTemplate(family, { open: chainOpen }) : nothing;
|
|
126
|
+
const kids = fam ? familyChipTemplate(fam, { open }) : nothing;
|
|
127
|
+
const famRows = fam && rows && open ? familyRowsTemplate(fam, { activeId, matchIds, archivable: !selectable }) : nothing;
|
|
130
128
|
// Its delegates and its chain's other links: the card is .contains-active while one is open.
|
|
131
129
|
const memberIds = [...(fam ? fam.members.map((m) => m.session.id) : []),
|
|
132
130
|
...(family?.chain ? family.chain.links.map((x) => x.id).filter((id) => id !== s.id) : [])];
|
|
@@ -165,10 +163,12 @@ export function stripTileTemplate(count) {
|
|
|
165
163
|
|
|
166
164
|
// The hidden strip's pill: "▾ 5 sessions · ● 1 live · ● 1 waiting"
|
|
167
165
|
// (strip.js stripShowParts).
|
|
166
|
+
// The count (and the dot after it) in its own span: a phone's narrow top
|
|
167
|
+
// bar drops it when live or waiting shows (index.html).
|
|
168
168
|
export function stripPillTemplate({ count, live, waiting }) {
|
|
169
|
-
const liveT = live ? html
|
|
170
|
-
const waitingT = waiting ? html
|
|
171
|
-
return html`<span class="pill-arrow" aria-hidden="true">\u25BE</span>
|
|
169
|
+
const liveT = live ? html`<span class="live"><span class="live-dot" aria-hidden="true"></span>${live}</span>` : nothing;
|
|
170
|
+
const waitingT = waiting ? html`${live ? " · " : ""}<span class="waiting"><span class="waiting-dot" aria-hidden="true"></span>${waiting}</span>` : nothing;
|
|
171
|
+
return html`<span class="pill-arrow" aria-hidden="true">\u25BE</span> <span class="pill-count">${count}${live || waiting ? " · " : ""}</span>${liveT}${waitingT}`;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
// Draws +template+ into +el+ as new nodes, as an innerHTML write did: no
|
|
@@ -274,7 +274,7 @@ export function createListsView(deps) {
|
|
|
274
274
|
win,
|
|
275
275
|
rowsFor: (id) => {
|
|
276
276
|
const f = stripFamilies.get(id);
|
|
277
|
-
return f && f.members.length ?
|
|
277
|
+
return f && f.members.length ? familyRowsTemplate(f, { activeId: selected() }) : null;
|
|
278
278
|
},
|
|
279
279
|
anchorFor: (id) => [...topStripEl.querySelectorAll(".card[data-id] .family-chip")].find((chip) => chip.closest(".card").dataset.id === id) || null,
|
|
280
280
|
onPick: (id) => {
|
|
@@ -302,7 +302,7 @@ export function createListsView(deps) {
|
|
|
302
302
|
win,
|
|
303
303
|
rowsFor: (id) => {
|
|
304
304
|
const f = chainFamilies.get(id);
|
|
305
|
-
return f?.chain ?
|
|
305
|
+
return f?.chain ? chainRowsTemplate(f, { activeId: selected() }) : null;
|
|
306
306
|
},
|
|
307
307
|
anchorFor: (id) => (chainFrom ? [...chainFrom.querySelectorAll(".card[data-id] .chain-chip")].find((chip) => chip.closest(".card").dataset.id === id) || null : null),
|
|
308
308
|
onPick: (id) => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// choice for the next create only (POST /api/sessions' llm_context), never
|
|
13
13
|
// remembered: nothing is sent unless the user sets one.
|
|
14
14
|
|
|
15
|
-
import { html, nothing } from "./vendor/lit-html.js";
|
|
15
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
16
16
|
|
|
17
17
|
export const STRATEGIES = [
|
|
18
18
|
{ value: "none", label: "none" },
|
|
@@ -149,9 +149,7 @@ export function strategyOptions(chosen) {
|
|
|
149
149
|
return [...STRATEGIES, { value: chosen, label: chosen }];
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
return `<option value="${escapeHtml(value)}"${value === chosen ? " selected" : ""}>${escapeHtml(label)}</option>`;
|
|
154
|
-
}
|
|
152
|
+
const option = (value, label) => html`<option value=${value}>${label}</option>`;
|
|
155
153
|
|
|
156
154
|
// The popover's form: the values now (each with its source), and the
|
|
157
155
|
// session's own to change.
|
|
@@ -161,30 +159,46 @@ export function nowText(summary) {
|
|
|
161
159
|
`budget ${budget} (${summary.budget_where})`;
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
//
|
|
165
|
-
// the values the form
|
|
166
|
-
|
|
162
|
+
// The popover's form (a lit-html template). For the new chat (kind
|
|
163
|
+
// NEW_CHAT), +summary+ is the model's and +choice+ the values the form
|
|
164
|
+
// shows; +now+ is the summary its "Now:" line reads (refresh passes a newer
|
|
165
|
+
// one, the rest stays as the form opened, so lit leaves what the user typed
|
|
166
|
+
// alone); +error+ a failed Set's message. The selects take their value as
|
|
167
|
+
// a property (.value), set after their options: +unset+ draws the options
|
|
168
|
+
// alone, the first pass renderForm makes.
|
|
169
|
+
export function formTemplate(summary, { running = false, kind = SESSION, choice = null, now = summary, error = null,
|
|
170
|
+
unset = false } = {}) {
|
|
167
171
|
const fresh = kind === NEW_CHAT;
|
|
168
172
|
const own = fresh ? ownValues({ own: choice }) : ownValues(summary);
|
|
169
173
|
const follow = (field) => (summary[`${field}_source`] === "session" ? "follow the model" : `follow the model (${summary[field]})`);
|
|
170
174
|
const when = fresh ? "For the new chat's first turn on; not remembered for the next one."
|
|
171
175
|
: "From the next turn's start. Turning forget on or off re-reads the whole prompt once.";
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
176
|
+
const chosen = (value) => (unset ? nothing : value);
|
|
177
|
+
return html`<form class="llmctx-form"><div class="ctx-pop-head"><strong>LLM context</strong
|
|
178
|
+
><span class="ctx-pop-kind">${fresh ? NEW_CHAT : "this session"}</span
|
|
179
|
+
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
180
|
+
><div class="ctx-pop-meta llmctx-now">${nowText(now)}</div
|
|
181
|
+
><label class="llmctx-field"><span>Strategy</span><select name="strategy" .value=${chosen(own.strategy)}>${
|
|
182
|
+
option(DEFAULT, follow("strategy"))}${strategyOptions(own.strategy).map((s) => option(s.value, s.label))}</select></label
|
|
183
|
+
><label class="llmctx-field"><span>Apply</span><select name="apply" .value=${chosen(own.apply)}>${
|
|
184
|
+
option(DEFAULT, follow("apply"))}${APPLIES.map((a) => option(a, a))}</select></label
|
|
185
|
+
><label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric" autocomplete="off"
|
|
186
|
+
placeholder="follow the model (64000, 64k or off)" .value=${own.budget}></label
|
|
187
|
+
><div class="ctx-pop-error llmctx-error" ?hidden=${!error}>${error || ""}</div
|
|
188
|
+
><div class="ctx-pop-meta">${when}${running ? " A turn is running: it applies after this turn." : ""}</div
|
|
189
|
+
><div class="ctx-pop-actions"><button type="button" class="ctx-pop-detach" data-act="reset">follow the model</button
|
|
190
|
+
><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Draws the form into +target+ (formTemplate's options). A first draw (no
|
|
194
|
+
// form there yet) makes two passes, the options and then the selects'
|
|
195
|
+
// values: lit sets an element's own bindings before its children, so a
|
|
196
|
+
// select's .value would land before its options exist. A later draw is
|
|
197
|
+
// one pass, and lit writes only what changed since the last (the "Now:"
|
|
198
|
+
// line, the error), never a value the user changed.
|
|
199
|
+
export function renderForm(target, summary, opts = {}) {
|
|
200
|
+
if (!target.firstElementChild) render(formTemplate(summary, { ...opts, unset: true }), target);
|
|
201
|
+
render(formTemplate(summary, opts), target);
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
const EDGE = 16;
|
|
@@ -212,7 +226,7 @@ export function popoverPlace(rect, { width, height, innerWidth, innerHeight }) {
|
|
|
212
226
|
// choice() the pending choice, and choose(next) takes a Set's (null for
|
|
213
227
|
// "follow the model") instead of a command running. id and anchor (the
|
|
214
228
|
// chip's selector) keep the two controls apart.
|
|
215
|
-
export function createLlmContextControl({ el,
|
|
229
|
+
export function createLlmContextControl({ el, summary, session = () => null, running = () => false, run,
|
|
216
230
|
kind = SESSION, choice = () => null, choose = null,
|
|
217
231
|
id = "llmContextPopover", anchor: anchorSel = ".llmctx-chip",
|
|
218
232
|
doc = document, win = window }) {
|
|
@@ -226,6 +240,14 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
226
240
|
// The values the form opened with, and the session it is for.
|
|
227
241
|
let initial = null;
|
|
228
242
|
let openFor = null;
|
|
243
|
+
// What the open form was drawn from: the summary, the form's options and
|
|
244
|
+
// the summary its "Now:" line reads. A closed popover is empty, so each
|
|
245
|
+
// open draws a fresh form (unsent edits are gone).
|
|
246
|
+
let shown = null;
|
|
247
|
+
const draw = (changes = {}) => {
|
|
248
|
+
shown = { ...shown, ...changes };
|
|
249
|
+
renderForm(pop, shown.data, { ...shown.opts, now: shown.now, error: shown.error });
|
|
250
|
+
};
|
|
229
251
|
|
|
230
252
|
function place(anchor) {
|
|
231
253
|
const width = Math.min(360, win.innerWidth - 32);
|
|
@@ -244,7 +266,8 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
244
266
|
if (!data) return;
|
|
245
267
|
initial = fresh ? ownValues({ own: choice() }) : ownValues(data);
|
|
246
268
|
openFor = session();
|
|
247
|
-
|
|
269
|
+
shown = { data, opts: { running: running(), kind, choice: choice() } };
|
|
270
|
+
draw({ now: data, error: null });
|
|
248
271
|
pop.hidden = false;
|
|
249
272
|
place(anchor);
|
|
250
273
|
}
|
|
@@ -253,19 +276,20 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
253
276
|
pop.hidden = true;
|
|
254
277
|
initial = null;
|
|
255
278
|
openFor = null;
|
|
279
|
+
shown = null;
|
|
280
|
+
render(nothing, pop);
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
function isOpen() { return !pop.hidden; }
|
|
259
284
|
|
|
260
285
|
// A new summary (a command_ran, a re-read): an open form keeps what the
|
|
261
|
-
// user typed; only its "Now:" line
|
|
262
|
-
// session closes it.
|
|
286
|
+
// user typed; only its "Now:" line follows (the same template again: lit
|
|
287
|
+
// writes that line alone). Another session closes it.
|
|
263
288
|
function refresh() {
|
|
264
289
|
if (!isOpen()) return;
|
|
265
290
|
const data = summary();
|
|
266
291
|
if (!data || session() !== openFor) return close();
|
|
267
|
-
|
|
268
|
-
if (now) now.textContent = nowText(data);
|
|
292
|
+
draw({ now: data });
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
el.addEventListener("click", (e) => {
|
|
@@ -277,8 +301,7 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
277
301
|
});
|
|
278
302
|
// An edit makes an error the last Set showed stale.
|
|
279
303
|
const clearError = () => {
|
|
280
|
-
|
|
281
|
-
if (error && !error.hidden) { error.hidden = true; error.textContent = ""; }
|
|
304
|
+
if (shown?.error) draw({ error: null });
|
|
282
305
|
};
|
|
283
306
|
pop.addEventListener("input", clearError);
|
|
284
307
|
pop.addEventListener("change", clearError);
|
|
@@ -292,9 +315,7 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
292
315
|
try {
|
|
293
316
|
next = mergeChoice(choice(), values, initial);
|
|
294
317
|
} catch (err) {
|
|
295
|
-
|
|
296
|
-
error.textContent = err.message;
|
|
297
|
-
error.hidden = false;
|
|
318
|
+
draw({ error: err.message });
|
|
298
319
|
return;
|
|
299
320
|
}
|
|
300
321
|
close();
|
|
@@ -102,6 +102,17 @@ function relayClosedText(relay, reason) {
|
|
|
102
102
|
return reason ? `Left open in ${relay.who} (${reason})` : `Left open in ${relay.who}`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Whether the card keeps the reply that asked it once resolved: the model's
|
|
106
|
+
// own question and an own approval (their call ran in the asking step). Not
|
|
107
|
+
// the step limit (asked between turns), a hook's question (not asked by a
|
|
108
|
+
// call) or a delegate's relayed approval (the parent's text is about its
|
|
109
|
+
// own call, the card's delegate line says what the child asks).
|
|
110
|
+
export function keepsAskingText(pq) {
|
|
111
|
+
if (!pq) return false;
|
|
112
|
+
if (isApproval(pq)) return !relayView(pq);
|
|
113
|
+
return !pq.kind || pq.kind === "question";
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
// The line a resolved card shows.
|
|
106
117
|
export function resultText(pq, { answer = null, cancelled = false, reason = "" } = {}) {
|
|
107
118
|
const approval = isApproval(pq);
|