samagotchi 0.154.0 → 0.155.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 +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- data/lib/samagotchi/pending_input_queue.rb +0 -55
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Finds and watches the fenced blocks a drawer may draw (docs/internals/block-renderers.md).
|
|
2
|
+
// One observer per turn root, filtered to TEXTS, as copy.js and answer_fold.js:
|
|
3
|
+
// every path that fills a root (a reload, a session switch, a turn's end, an
|
|
4
|
+
// answer_display, Revise's clones, a step's swap) arrives the same way, and
|
|
5
|
+
// cards, the asking slot and question replies are never touched. A block is
|
|
6
|
+
// drawn when it comes near its scroller's view (a closed <details> opening is
|
|
7
|
+
// such a coming near), and one that moves to another scroller while it waits
|
|
8
|
+
// (the stage's hand-off to #history) is watched there.
|
|
9
|
+
//
|
|
10
|
+
// What draws is a drawer's: block_host.js's frames for a bundle's renderers,
|
|
11
|
+
// after any core drawers. The DOM is the state, as block_source.js says: a
|
|
12
|
+
// figure right before the <pre> says it is drawn, data-block-failed on the
|
|
13
|
+
// <pre> says it isn't to be tried again.
|
|
14
|
+
//
|
|
15
|
+
// The drawer contract:
|
|
16
|
+
// - claims(lang): true when it draws this language. A Promise of that when it
|
|
17
|
+
// can't say yet (the bundle drawer's list): the next drawer is asked only
|
|
18
|
+
// after it says no. The first that claims draws; a language no drawer claims
|
|
19
|
+
// is left alone.
|
|
20
|
+
// - needsDraw(pre, figure): whether the block is to be watched and drawn.
|
|
21
|
+
// +figure+ is its drawn figure or null: a drawn block comes here too (the
|
|
22
|
+
// frame drawer redraws one in another theme). It may settle the block on the
|
|
23
|
+
// spot (a cache hit placed, a refusal's line) and say false.
|
|
24
|
+
// - draw(pre, { reconsider }): draws it now; a Promise while it runs, during
|
|
25
|
+
// which the block isn't watched again. reconsider(pre) has the watcher look
|
|
26
|
+
// at the block afresh (a draw that has to try again).
|
|
27
|
+
// - sessionChanged() (optional): the open session changed.
|
|
28
|
+
// A draw that throws or rejects fails its block (failBlock, logged); it never
|
|
29
|
+
// stops the other blocks.
|
|
30
|
+
//
|
|
31
|
+
// What a drawer must keep to:
|
|
32
|
+
// - What it draws goes in a <figure class="block-drawn"> placed directly before
|
|
33
|
+
// the <pre> (the <pre> is its nextElementSibling): figureOf, the toggle,
|
|
34
|
+
// rejoin and a Revise clone all read it there. The <pre> stays, hidden.
|
|
35
|
+
// - holdingView needs the turn roots: the drawer is given them by its host.
|
|
36
|
+
// - reconsider(pre) called during a synchronous part of draw starts a new draw
|
|
37
|
+
// nested in this one (with a watch that fires at once, as the specs' does).
|
|
38
|
+
//
|
|
39
|
+
// rejoin, failBlock, holdingView and scrollerOf are this module's exports, not
|
|
40
|
+
// block_source.js's: new exports go in new modules (web-frontend.md).
|
|
41
|
+
//
|
|
42
|
+
// A module of its own: static JS is cached at unversioned URLs, and a new
|
|
43
|
+
// export on an old module would blank the page.
|
|
44
|
+
|
|
45
|
+
import { DRAWN, figureOf, setSourceShown } from "./block_source.js";
|
|
46
|
+
import { copyText } from "./copy.js";
|
|
47
|
+
import { keepFollowing, shouldFollowScroll } from "./scroll.js";
|
|
48
|
+
import { TEXTS } from "./text_roots.js";
|
|
49
|
+
|
|
50
|
+
const BLOCKS = "pre[data-lang]";
|
|
51
|
+
const IO_MARGIN = "600px 0px";
|
|
52
|
+
|
|
53
|
+
// The nearest scroller holding +el+, +root+ at the latest. IntersectionObserver
|
|
54
|
+
// needs it named: with root:null a rootMargin doesn't pass an overflow:auto
|
|
55
|
+
// between, so a block just under #history's fold wouldn't count.
|
|
56
|
+
export function scrollerOf(el, root, win = window) {
|
|
57
|
+
for (let at = el.parentElement; at && at !== root; at = at.parentElement) {
|
|
58
|
+
const { overflowY } = win.getComputedStyle(at);
|
|
59
|
+
if (overflowY === "auto" || overflowY === "scroll") return at;
|
|
60
|
+
}
|
|
61
|
+
return root;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rootOf = (roots, el) => roots.find((root) => root.contains(el)) || roots[0];
|
|
65
|
+
|
|
66
|
+
// +mutate+ with the view held: a viewer at the bottom stays there, and a
|
|
67
|
+
// block above a reader's view keeps what they read where it is. Browsers that
|
|
68
|
+
// anchor scrolling have done it by the second read, and there is nothing left.
|
|
69
|
+
// @param bottomOf () => the inserted thing's bottom, after +mutate+
|
|
70
|
+
export function holdingView(pre, roots, mutate, bottomOf, win = window) {
|
|
71
|
+
const scroller = scrollerOf(pre, rootOf(roots, pre), win);
|
|
72
|
+
if (shouldFollowScroll(scroller)) return keepFollowing(scroller, mutate);
|
|
73
|
+
const before = pre.getBoundingClientRect().bottom;
|
|
74
|
+
if (before > scroller.getBoundingClientRect().top) return mutate();
|
|
75
|
+
const result = mutate();
|
|
76
|
+
const after = bottomOf();
|
|
77
|
+
if (after !== before) scroller.scrollTop += after - before;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The <pre> stays as it is, under one muted line: never markup (the words are
|
|
82
|
+
// a drawer's, driven by the model's input), and shown by CSS, so a bubble's
|
|
83
|
+
// textContent, its dedupe and an annotation's text stay what they were (copy.js's
|
|
84
|
+
// rule for its buttons). A click on the line copies it (the watcher's listener).
|
|
85
|
+
export function failBlock(pre, lang, message) {
|
|
86
|
+
if (!pre.isConnected) return;
|
|
87
|
+
pre.dataset.blockFailed = "1";
|
|
88
|
+
const note = pre.ownerDocument.createElement("div");
|
|
89
|
+
note.className = "block-note";
|
|
90
|
+
const first = String(message ?? "").split("\n")[0].trim();
|
|
91
|
+
const words = `Couldn't draw this ${lang} block${first ? `: ${first}` : ""}`;
|
|
92
|
+
note.dataset.text = words;
|
|
93
|
+
note.setAttribute("role", "note");
|
|
94
|
+
note.setAttribute("aria-label", words);
|
|
95
|
+
note.title = "Click to copy";
|
|
96
|
+
pre.after(note);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A copy.js wrap took the <pre> away from a figure placed first: back with it.
|
|
100
|
+
export function rejoin(pre) {
|
|
101
|
+
if (figureOf(pre) || !pre.hidden) return;
|
|
102
|
+
const wrap = pre.parentElement;
|
|
103
|
+
const stray = wrap?.classList.contains("code-wrap") && wrap.previousElementSibling;
|
|
104
|
+
if (stray?.classList?.contains(DRAWN)) wrap.insertBefore(stray, pre);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// One IntersectionObserver per scroller. +el+ moved to another scroller (the
|
|
108
|
+
// stage's hand-off to the history) moves with it.
|
|
109
|
+
function ioWatcher(win) {
|
|
110
|
+
const observers = new Map();
|
|
111
|
+
const watched = new WeakMap();
|
|
112
|
+
return (el, scroller, onNear) => {
|
|
113
|
+
let io = observers.get(scroller);
|
|
114
|
+
if (!io) {
|
|
115
|
+
io = new win.IntersectionObserver((entries) => {
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (!entry.isIntersecting) continue;
|
|
118
|
+
io.unobserve(entry.target);
|
|
119
|
+
const near = watched.get(entry.target);
|
|
120
|
+
watched.delete(entry.target);
|
|
121
|
+
near?.onNear();
|
|
122
|
+
}
|
|
123
|
+
}, { root: scroller, rootMargin: IO_MARGIN });
|
|
124
|
+
observers.set(scroller, io);
|
|
125
|
+
}
|
|
126
|
+
const before = watched.get(el);
|
|
127
|
+
if (before && before.io !== io) before.io.unobserve(el);
|
|
128
|
+
watched.set(el, { io, onNear });
|
|
129
|
+
io.observe(el);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// @param roots the turn roots (app.js turnRoots())
|
|
134
|
+
// @param sessionId () => the open session's id: a change tells the drawers
|
|
135
|
+
// @param drawers in order: the first that claims a block's language draws it
|
|
136
|
+
// @param watch (el, scroller, onNear) => calls onNear once el comes near; a seam
|
|
137
|
+
// for the specs
|
|
138
|
+
export function createBlockWatch({ roots, sessionId, drawers, watch = null, copy = copyText, win = window }) {
|
|
139
|
+
const near = watch || ioWatcher(win);
|
|
140
|
+
// Blocks with a draw on its way (not a drawn state: that is the figure): pre =>
|
|
141
|
+
// {target, scroller, start, drawing}. Until it starts, a block whose scroller
|
|
142
|
+
// changed (the stage's hand-off to the history) gets watched there.
|
|
143
|
+
const waiting = new WeakMap();
|
|
144
|
+
let session = sessionId();
|
|
145
|
+
|
|
146
|
+
function noticeSession() {
|
|
147
|
+
const now = sessionId();
|
|
148
|
+
if (now === session) return;
|
|
149
|
+
session = now;
|
|
150
|
+
for (const drawer of drawers) drawer.sessionChanged?.();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The drawer claiming +lang+, to +fn+: at once when each one asked can say.
|
|
154
|
+
// A throw in +fn+ after an asynchronous claim goes to +onError+.
|
|
155
|
+
function pick(lang, fn, onError, from = 0) {
|
|
156
|
+
for (let i = from; i < drawers.length; i++) {
|
|
157
|
+
const drawer = drawers[i];
|
|
158
|
+
const claimed = drawer.claims(lang);
|
|
159
|
+
if (claimed?.then) {
|
|
160
|
+
claimed
|
|
161
|
+
.then((yes) => (yes ? fn(drawer) : pick(lang, fn, onError, i + 1)), () => pick(lang, fn, onError, i + 1))
|
|
162
|
+
.catch(onError);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (claimed) { fn(drawer); return; }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function watchFor(entry) {
|
|
170
|
+
const scroller = scrollerOf(entry.target, rootOf(roots, entry.target), win);
|
|
171
|
+
if (scroller === entry.scroller) return;
|
|
172
|
+
entry.scroller = scroller;
|
|
173
|
+
near(entry.target, scroller, entry.start);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function reconsider(pre) {
|
|
177
|
+
if (waiting.get(pre)?.drawing) waiting.delete(pre);
|
|
178
|
+
consider(pre);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A drawer that throws or rejects fails its block, with its line; the rest of
|
|
182
|
+
// the observer's batch still draws.
|
|
183
|
+
function broke(pre, e) {
|
|
184
|
+
console.error("block drawer:", e);
|
|
185
|
+
if (!pre.dataset.blockFailed) failBlock(pre, pre.getAttribute("data-lang"), e?.message);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function start(entry, pre, drawer) {
|
|
189
|
+
entry.drawing = true;
|
|
190
|
+
const over = () => { if (waiting.get(pre) === entry) waiting.delete(pre); };
|
|
191
|
+
let drawing;
|
|
192
|
+
try {
|
|
193
|
+
drawing = drawer.draw(pre, { reconsider });
|
|
194
|
+
} catch (e) {
|
|
195
|
+
over();
|
|
196
|
+
broke(pre, e);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
Promise.resolve(drawing).then(over, (e) => { over(); broke(pre, e); });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function consider(pre) {
|
|
203
|
+
if (!pre.isConnected || !pre.closest(TEXTS) || pre.dataset.blockFailed) return;
|
|
204
|
+
const pending = waiting.get(pre);
|
|
205
|
+
if (pending) {
|
|
206
|
+
if (!pending.drawing) watchFor(pending);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
rejoin(pre);
|
|
210
|
+
const lang = pre.getAttribute("data-lang");
|
|
211
|
+
if (!pre.textContent.trim()) return; // an empty fence is left alone
|
|
212
|
+
pick(lang, (drawer) => {
|
|
213
|
+
if (!pre.isConnected || waiting.has(pre) || pre.dataset.blockFailed) return;
|
|
214
|
+
const figure = figureOf(pre);
|
|
215
|
+
if (!drawer.needsDraw(pre, figure)) return;
|
|
216
|
+
// A drawn block's <pre> is hidden, which never intersects: its figure does.
|
|
217
|
+
const entry = { target: figure || pre, scroller: null, drawing: false, start: () => start(entry, pre, drawer) };
|
|
218
|
+
waiting.set(pre, entry);
|
|
219
|
+
watchFor(entry);
|
|
220
|
+
}, (e) => broke(pre, e));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function scan(el) {
|
|
224
|
+
if (el.matches?.(BLOCKS)) consider(el);
|
|
225
|
+
for (const pre of el.querySelectorAll?.(BLOCKS) || []) consider(pre);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const root of roots) {
|
|
229
|
+
new win.MutationObserver((records) => {
|
|
230
|
+
noticeSession();
|
|
231
|
+
for (const record of records) {
|
|
232
|
+
for (const node of record.addedNodes) if (node.nodeType === 1) scan(node);
|
|
233
|
+
}
|
|
234
|
+
}).observe(root, { childList: true, subtree: true });
|
|
235
|
+
scan(root);
|
|
236
|
+
|
|
237
|
+
root.addEventListener("mousedown", (e) => {
|
|
238
|
+
if (e.target.closest?.(".block-toggle")) e.preventDefault();
|
|
239
|
+
});
|
|
240
|
+
// Delegated, so a Revise clone's work too.
|
|
241
|
+
root.addEventListener("click", (e) => {
|
|
242
|
+
const toggle = e.target.closest?.(".block-toggle");
|
|
243
|
+
if (toggle) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
e.stopPropagation();
|
|
246
|
+
const figure = toggle.parentElement;
|
|
247
|
+
setSourceShown(figure, figure.nextElementSibling, !figure.classList.contains("source"));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const note = e.target.closest?.(".block-note");
|
|
251
|
+
if (note) copy(note.dataset.text);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { scan, reconsider };
|
|
256
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Where a snapshot's card or notice goes by its stamp: the id of the turn
|
|
2
|
+
// opening it comes after (+opening_id+, Bridge::CardPlaces#opening_of: a
|
|
3
|
+
// prompt's turn_id, a wake note's, /continue no's reason line's own id). The
|
|
4
|
+
// page finds that opening's bubble (data-turn-id on a prompt, data-opening-id
|
|
5
|
+
// on a wake note or a reason line, turn_html.js) and the entry goes before
|
|
6
|
+
// the next turn's opening, or at the end. No counting, so the page and the
|
|
7
|
+
// worker can't disagree on which bubbles open turns. An entry with no stamp
|
|
8
|
+
// (an older worker's, one before every opening), or whose opening the page
|
|
9
|
+
// doesn't draw, falls back to its count (card.js cardPlace, turns_since).
|
|
10
|
+
// A new module: the static JS is unversioned (docs/internals/web-frontend.md).
|
|
11
|
+
import { cardPlace } from "./card.js";
|
|
12
|
+
|
|
13
|
+
// The opening bubble +id+ names among the turns' openings (oldest first,
|
|
14
|
+
// bubbles.js turnPromptBubbles), or -1.
|
|
15
|
+
export function stampedOpening(userBubbles, id) {
|
|
16
|
+
if (!id) return -1;
|
|
17
|
+
const stamp = String(id);
|
|
18
|
+
return userBubbles.findIndex((el) => el.dataset?.turnId === stamp || el.dataset?.openingId === stamp);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The element +entry+ goes before: the opening after its stamped one (null:
|
|
22
|
+
// the end), else what its count says (null: the end too).
|
|
23
|
+
export function snapshotPlace(userBubbles, entry) {
|
|
24
|
+
const at = stampedOpening(userBubbles, entry?.opening_id);
|
|
25
|
+
if (at < 0) return cardPlace(userBubbles, entry);
|
|
26
|
+
return userBubbles[at + 1] || null;
|
|
27
|
+
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// above the first turn after them, a finished turn's notice in its step, a
|
|
9
9
|
// finished turn's question resolved where it was asked.
|
|
10
10
|
import { getSession, sendCommand } from "./data.js";
|
|
11
|
-
import {
|
|
11
|
+
import { drawCard, isCard, isNoticeCard, refreshCardBody, turnNoticePlace } from "./card.js";
|
|
12
|
+
import { snapshotPlace } from "./card_stamps.js";
|
|
12
13
|
import { elementOf } from "./turn_html.js";
|
|
13
14
|
import { stepText } from "./turn_block.js";
|
|
14
15
|
import { llmContextNotice, noticeLine } from "./turn_events.js";
|
|
@@ -83,14 +84,15 @@ export function createCardsPlace({
|
|
|
83
84
|
bubbles.placeFollowing(el, before);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
// A snapshot's cards and notices, each above the first turn after it
|
|
87
|
+
// A snapshot's cards and notices, each above the first turn after it (the
|
|
88
|
+
// one after its stamped opening, card_stamps.js; else by its count); a
|
|
87
89
|
// turn's own notice where it showed live (placeTurnNotice). +end+: what
|
|
88
90
|
// closes the history below them (a failed turn's prompt), for those with
|
|
89
91
|
// no turn after them; one shown after that prompt went back stays below it.
|
|
90
92
|
function placeSnapshotCards(entries, { before: end = null } = {}) {
|
|
91
93
|
const users = turnPromptBubbles(historyEl);
|
|
92
94
|
for (const entry of entries) {
|
|
93
|
-
const before =
|
|
95
|
+
const before = snapshotPlace(users, entry) || (entry.after_failed ? null : end);
|
|
94
96
|
if (isCard(entry)) upsertCard(entry, { before });
|
|
95
97
|
else if (entry.type === "question") placeResolvedQuestion(entry, before);
|
|
96
98
|
else if (entry.type === "guardrail_warning") bubbles.showGuardrailWarning(entry.message, entry.label || "guardrails", { before });
|
|
@@ -53,7 +53,10 @@ export function opensOnKey(e, { active, allView, lightbox }) {
|
|
|
53
53
|
return !editable(active);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// A field inside a shadow root (a chi-ui widget's note, inline_widgets.js)
|
|
57
|
+
// counts: document.activeElement is its host, so the look goes through.
|
|
56
58
|
function editable(el) {
|
|
59
|
+
while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
|
|
57
60
|
if (!el) return false;
|
|
58
61
|
if (el.isContentEditable) return true;
|
|
59
62
|
const tag = (el.tagName || "").toUpperCase();
|
|
@@ -51,7 +51,7 @@ export function composerMode({ selected, owner, running, narrow }) {
|
|
|
51
51
|
if (tui) {
|
|
52
52
|
return {
|
|
53
53
|
...base,
|
|
54
|
-
placeholder: pick("A terminal chi owns this session ·
|
|
54
|
+
placeholder: pick("A terminal chi owns this session · a one-turn chi --non-interactive run or an older chi's terminal", "A terminal chi owns this session"),
|
|
55
55
|
hint: "", button: "Send", disabled: true,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -97,13 +97,14 @@ export function deleteConfirmText(session) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// The badge on a session a process holds right now (the list's `owner`):
|
|
100
|
-
// a worker (every UI can send), or a terminal chi
|
|
100
|
+
// a worker (every UI can send), or a terminal chi: a one-turn --non-interactive
|
|
101
|
+
// run or an older chi's terminal, in OwnerLock::TUI_BUSY's words.
|
|
101
102
|
export function ownerBadge(owner, id) {
|
|
102
103
|
if (owner === "worker") {
|
|
103
104
|
return { kind: "worker", text: "live", title: `A worker runs this session; a terminal joins with: chi --attach ${id}` };
|
|
104
105
|
}
|
|
105
106
|
if (owner === "tui") {
|
|
106
|
-
return { kind: "tui", text: "terminal", title: "
|
|
107
|
+
return { kind: "tui", text: "terminal", title: "In use by a one-turn chi --non-interactive run or an older chi's terminal" };
|
|
107
108
|
}
|
|
108
109
|
return null;
|
|
109
110
|
}
|
|
@@ -1119,14 +1119,28 @@ button.ws-row:hover:not(:disabled){filter:none}@media(hover:hover){button.ws-row
|
|
|
1119
1119
|
#infoBar .meta .bar-group{display:flex;gap:8px;align-items:center;min-width:0;flex:none}
|
|
1120
1120
|
#infoBar .meta .bar-sep{flex:none;width:1px;height:14px;background:var(--border-strong)}
|
|
1121
1121
|
/* Above the phone's one scrolling line the meta is one line too: the telemetry takes the room left and its folder gives
|
|
1122
|
-
way first (it already loses its start); a chip is capped, cut with "…", their full
|
|
1122
|
+
way first (it already loses its start), then the model chip, down to 10ch; a chip is capped, cut with "…", their full
|
|
1123
|
+
text in the title. The two groups are column grids: the giving chip's track (minmax(…, max-content)) leaves the
|
|
1124
|
+
other chips as the group's min-content, so a group never shrinks under its fixed chips (they once slid under
|
|
1125
|
+
continue →), and the info keeps that min-width: past it the bar's buttons wrap to a line of their own. */
|
|
1123
1126
|
@media(min-width:601px){
|
|
1127
|
+
#infoBar .info{min-width:auto}
|
|
1124
1128
|
#infoBar .meta{flex-wrap:nowrap}
|
|
1125
|
-
#infoBar .meta .bar-group.telemetry{
|
|
1126
|
-
#infoBar .meta .
|
|
1127
|
-
#infoBar .meta .telemetry
|
|
1129
|
+
#infoBar .meta .bar-group:is(.setup,.telemetry){display:grid;grid-auto-flow:column;grid-auto-columns:max-content}
|
|
1130
|
+
#infoBar .meta .bar-group.setup{flex:0 1 auto;min-width:auto;grid-template-columns:max-content max-content minmax(10ch,max-content)}
|
|
1131
|
+
#infoBar .meta .bar-group.telemetry{flex:1 1 0;min-width:auto;grid-template-columns:minmax(0,max-content)}
|
|
1132
|
+
#infoBar .meta .telemetry>.dir{max-width:48ch}
|
|
1128
1133
|
#infoBar .meta .model{max-width:40ch;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1129
1134
|
}
|
|
1135
|
+
/* Below 900px the buttons already have a line of their own, and the meta's one line can be wider than the bar: its
|
|
1136
|
+
groups wrap instead of being cut, the telemetry first. The setup's basis is 0 (its min-content, the model at 10ch)
|
|
1137
|
+
and its grow wins up to its max-content, so a group moves down only when the min-contents don't fit; the room left
|
|
1138
|
+
is the telemetry's (its folder). The info never asks for more than the bar. */
|
|
1139
|
+
@media(min-width:601px) and (max-width:899px){
|
|
1140
|
+
#infoBar .info{max-width:100%}
|
|
1141
|
+
#infoBar .meta{flex-wrap:wrap;row-gap:4px}
|
|
1142
|
+
#infoBar .meta .bar-group.setup{flex:1000 1 0;max-width:max-content}
|
|
1143
|
+
}
|
|
1130
1144
|
/* The plain chips (spans, links) and the status take the button chips' 1px border, transparent and outside their tint, so every chip is one height: a chip wrapping onto a row of plain ones doesn't grow it. */
|
|
1131
1145
|
#infoBar .meta :is(span,a).model,#infoBar .meta .status{border:1px solid transparent;background-clip:padding-box}
|
|
1132
1146
|
/* The chips a running turn rewrites (the session clock each second, the last generation's speed, ctx): tabular digits in a reserved width, so a tick ("9.5s" → "10s" → "1m 00s") or a new number doesn't widen the line and move the chips after it. The speed and the clock show from the session's start, a muted placeholder until there is a number. */
|
|
@@ -1143,6 +1157,9 @@ button.ws-row:hover:not(:disabled){filter:none}@media(hover:hover){button.ws-row
|
|
|
1143
1157
|
#infoBar .meta .worker-restart{padding:1px 8px;border-radius:5px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
1144
1158
|
@media(hover:hover){#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}}
|
|
1145
1159
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
1160
|
+
/* The bar's action buttons are one flex item: when they don't fit beside the info line they wrap to a line of their
|
|
1161
|
+
own together, never one button alone. */
|
|
1162
|
+
#infoBar .info-actions{display:flex;flex:none;gap:10px;align-items:center}
|
|
1146
1163
|
#infoBar button{padding:4px 12px;border-radius:8px;font-size:12px;font-weight:600}
|
|
1147
1164
|
#infoBar #infoStopBtn{background:var(--bg-raised);color:var(--warn-text);border:1px solid var(--border-strong)}
|
|
1148
1165
|
@media(hover:hover){#infoBar #infoStopBtn:hover{background:var(--warn-bg);border-color:var(--warn-border)}}
|
|
@@ -1368,7 +1385,7 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1368
1385
|
#infoBar .meta .bar-group{display:contents}
|
|
1369
1386
|
#infoBar .meta .bar-sep{display:none}
|
|
1370
1387
|
#infoBar .meta :is(.tools-chip,.notes-chip,.mem-chip,.ws-chip,.chain-link,.parent,.children-chip,.participants-chip,.dir,.ctx,.speed,.session-time,.worker-badge){display:none}
|
|
1371
|
-
#infoBar
|
|
1388
|
+
#infoBar .info-actions{display:none}
|
|
1372
1389
|
#infoBar .meta .status+.model{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
1373
1390
|
/* ⚙ alone, the start page's dot when the session set its own (its values in the tooltip and accessible name). */
|
|
1374
1391
|
#infoBar .meta .settings-chip .sc-own{display:none}
|
|
@@ -1835,10 +1852,12 @@ a{color:var(--accent)}
|
|
|
1835
1852
|
<div id="infoBar" class="composer-card-foot">
|
|
1836
1853
|
<div class="info" id="infoText"></div>
|
|
1837
1854
|
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
1838
|
-
<
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1855
|
+
<div class="info-actions" id="infoActions">
|
|
1856
|
+
<button class="ghost" id="infoContinueBtn" title="Continue: start the next link of this session's chain from its recap (this one is archived)" style="display:none">continue →</button>
|
|
1857
|
+
<button class="ghost" id="infoArchiveBtn" title="Archive: hide from the lists, keep for good" style="display:none">archive</button>
|
|
1858
|
+
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop session</button>
|
|
1859
|
+
<button class="ghost" id="infoDeleteBtn" title="Delete session (for good)" style="display:none">delete</button>
|
|
1860
|
+
</div>
|
|
1842
1861
|
<button type="button" class="ghost" id="infoMoreBtn" data-anchor-stand-in aria-label="More" title="More" aria-haspopup="dialog" aria-expanded="false"><span aria-hidden="true">⋯</span></button>
|
|
1843
1862
|
</div>
|
|
1844
1863
|
</div>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// The core drawer of chi-ui blocks (docs/internals/inline-widgets.md): a picks
|
|
2
|
+
// or a walk, drawn from the data the server parsed (the <pre>'s data-widget,
|
|
3
|
+
// InlineWidgets on the server), asked before the bundle renderers by
|
|
4
|
+
// block_watch.js (its header has the drawer contract).
|
|
5
|
+
//
|
|
6
|
+
// What it draws: a <figure class="block-drawn"> right before the <pre>, which
|
|
7
|
+
// stays, hidden (the DOM is the state, block_source.js), holding the shared
|
|
8
|
+
// source toggle and a .widget-host whose open shadow root holds the widget and
|
|
9
|
+
// its stylesheet (widgets.css, fetched once into a constructable sheet the
|
|
10
|
+
// widgets adopt, so none draws unstyled and the view held measures the
|
|
11
|
+
// styled widget). The toggle is in the light DOM: a click in the shadow root
|
|
12
|
+
// reaches the page's delegated listeners retargeted at the host. In a shadow
|
|
13
|
+
// root the widget's words are out of the bubble's textContent, which the
|
|
14
|
+
// shown-texts dedupe, copy's fallback and an annotation's quote read, as with a
|
|
15
|
+
// renderer's <img>.
|
|
16
|
+
//
|
|
17
|
+
// cloneNode doesn't copy a shadow root: a Revise clone (revise_switch.js
|
|
18
|
+
// viewOf) arrives with the figure and an empty host, which needsDraw sees and
|
|
19
|
+
// draw fills again (a fresh widget, its listeners its own).
|
|
20
|
+
//
|
|
21
|
+
// A refused block (data-widget-error) stays source under failBlock's muted line;
|
|
22
|
+
// a <pre> with neither attribute (an older server, web.markdown off never has a
|
|
23
|
+
// data-lang) is left alone. The model's words reach the page as text only.
|
|
24
|
+
//
|
|
25
|
+
// A module of its own: static JS is cached at unversioned URLs.
|
|
26
|
+
|
|
27
|
+
import { html, render as litRender } from "./vendor/lit-html.js";
|
|
28
|
+
import { DRAWN, figureOf, setSourceShown } from "./block_source.js";
|
|
29
|
+
import { failBlock, holdingView } from "./block_watch.js";
|
|
30
|
+
import { drawPicks, withDraft } from "./widget_picks.js";
|
|
31
|
+
import { drawWalk } from "./widget_walk.js";
|
|
32
|
+
|
|
33
|
+
export const LANG = "chi-ui";
|
|
34
|
+
export const HOST = "widget-host";
|
|
35
|
+
export const STYLESHEET = "/assets/widgets.css";
|
|
36
|
+
export const TOGGLE_PICTURE = "Show the widget";
|
|
37
|
+
|
|
38
|
+
// widgets.css as a constructable sheet, fetched once per page; a failed fetch
|
|
39
|
+
// draws unstyled and is tried again by the next draw.
|
|
40
|
+
let shared = null;
|
|
41
|
+
export function loadWidgetSheet(win = window) {
|
|
42
|
+
shared ??= win.fetch(STYLESHEET, { headers: { Accept: "text/css" } })
|
|
43
|
+
.then((res) => {
|
|
44
|
+
if (!res.ok) throw new Error(`widgets.css answered ${res.status}`);
|
|
45
|
+
return res.text();
|
|
46
|
+
})
|
|
47
|
+
.catch((e) => {
|
|
48
|
+
console.error("widgets.css:", e);
|
|
49
|
+
shared = null;
|
|
50
|
+
return "";
|
|
51
|
+
})
|
|
52
|
+
.then((css) => {
|
|
53
|
+
const sheet = new win.CSSStyleSheet();
|
|
54
|
+
sheet.replaceSync(css);
|
|
55
|
+
return sheet;
|
|
56
|
+
});
|
|
57
|
+
return shared;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// block_host.js's toggle icon, the same button.
|
|
61
|
+
const ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M5.5 4 2 8l3.5 4M10.5 4 14 8l-3.5 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
62
|
+
|
|
63
|
+
const DRAW = {
|
|
64
|
+
walk: (data, { doc }) => drawWalk(data, doc),
|
|
65
|
+
picks: (data, { doc, fill }) => drawPicks(data, { doc, fill }),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// The widget's data on +pre+.
|
|
69
|
+
// @throws Error it isn't JSON of a known type
|
|
70
|
+
export function widgetData(pre) {
|
|
71
|
+
const data = JSON.parse(pre.getAttribute("data-widget"));
|
|
72
|
+
if (!data || !Object.hasOwn(DRAW, data.type)) throw new Error(`no widget of type ${String(data?.type).slice(0, 40)}`);
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The host of a drawn figure, or null.
|
|
77
|
+
export function hostOf(figure) {
|
|
78
|
+
return figure?.querySelector(`:scope > .${HOST}`) || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// @param roots the turn roots, for the view held
|
|
82
|
+
// @param composerText () => the composer's text now
|
|
83
|
+
// @param fillComposer (text) => the composer filled with +text+ (app.js's)
|
|
84
|
+
// @param sheet () => a Promise of the widgets' CSSStyleSheet (a seam for the specs)
|
|
85
|
+
export function createWidgetDrawer({ roots, composerText, fillComposer, doc = document, win = window, sheet = () => loadWidgetSheet(win) }) {
|
|
86
|
+
const fill = (reply) => fillComposer(withDraft(composerText(), reply));
|
|
87
|
+
|
|
88
|
+
function fillHost(host, data, styles) {
|
|
89
|
+
const shadow = host.shadowRoot || host.attachShadow({ mode: "open" });
|
|
90
|
+
shadow.adoptedStyleSheets = [styles];
|
|
91
|
+
shadow.replaceChildren(DRAW[data.type](data, { doc, fill }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function makeFigure(data) {
|
|
95
|
+
const figure = doc.createElement("figure");
|
|
96
|
+
figure.className = `${DRAWN} widget`;
|
|
97
|
+
figure.dataset.widgetType = data.type;
|
|
98
|
+
figure.dataset.togglePicture = TOGGLE_PICTURE;
|
|
99
|
+
const host = doc.createElement("div");
|
|
100
|
+
host.className = HOST;
|
|
101
|
+
const toggle = doc.createElement("button");
|
|
102
|
+
toggle.type = "button";
|
|
103
|
+
toggle.className = "block-toggle";
|
|
104
|
+
litRender(ICON, toggle);
|
|
105
|
+
figure.append(host, toggle);
|
|
106
|
+
return figure;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function needsDraw(pre, figure) {
|
|
110
|
+
if (figure) return !hostOf(figure)?.shadowRoot;
|
|
111
|
+
if (pre.hasAttribute("data-widget-error")) {
|
|
112
|
+
failBlock(pre, LANG, pre.getAttribute("data-widget-error"));
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return pre.hasAttribute("data-widget");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// After the sheet: the data is read first, so a bad block fails at once.
|
|
119
|
+
function draw(pre) {
|
|
120
|
+
const data = widgetData(pre);
|
|
121
|
+
return sheet().then((styles) => place(pre, data, styles));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function place(pre, data, styles) {
|
|
125
|
+
if (!pre.isConnected) return;
|
|
126
|
+
const drawn = figureOf(pre);
|
|
127
|
+
if (drawn) {
|
|
128
|
+
let host = hostOf(drawn);
|
|
129
|
+
if (!host) {
|
|
130
|
+
host = doc.createElement("div");
|
|
131
|
+
host.className = HOST;
|
|
132
|
+
drawn.prepend(host);
|
|
133
|
+
}
|
|
134
|
+
if (!host.shadowRoot) fillHost(host, data, styles);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const figure = makeFigure(data);
|
|
138
|
+
fillHost(hostOf(figure), data, styles);
|
|
139
|
+
holdingView(pre, roots, () => {
|
|
140
|
+
pre.before(figure);
|
|
141
|
+
setSourceShown(figure, pre, false);
|
|
142
|
+
}, () => figure.getBoundingClientRect().bottom, win);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { claims: (lang) => lang === LANG, needsDraw, draw };
|
|
146
|
+
}
|
|
@@ -98,7 +98,7 @@ export function createOwnerVerify({ historyEl, store, api, claim, liveTurn, toas
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Verify's claim: a selection in an ended turn's final answer (the only
|
|
101
|
-
// text
|
|
101
|
+
// text TurnStages::VerifyAnchors#verified takes; verify_target.js), else null and no
|
|
102
102
|
// Verify on the bar.
|
|
103
103
|
function anchorFor({ source, range }) {
|
|
104
104
|
if (source?.kind !== "answer" || !range) return null;
|
|
@@ -690,7 +690,7 @@ const annotateBar = createAnnotateBar({
|
|
|
690
690
|
// Verify's claim: a selection in an ended turn's final reply (the last
|
|
691
691
|
// reply bubble after the turn's opening, as the owner's page and
|
|
692
692
|
// placeVerdict count turns: verify_target.js), the only text
|
|
693
|
-
//
|
|
693
|
+
// TurnStages::VerifyAnchors#verified takes; else null and no Verify on the bar. Not an
|
|
694
694
|
// earlier reply, a line merged mid-turn, a prompt, the running turn or the
|
|
695
695
|
// Plan.
|
|
696
696
|
function verifyAnchorFor({ source, range }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Revise stacks (web): a Revise turn rewrites an earlier turn's answer, and
|
|
2
2
|
// the server stamps its prompt with `revises: {turn_id}` once it completed
|
|
3
|
-
// with an answer (
|
|
3
|
+
// with an answer (TurnStages::VerifyAnchors#stamp_opening; never derived here). The page draws
|
|
4
4
|
// each stack v1 ← v2 ← … ← top in place:
|
|
5
5
|
//
|
|
6
6
|
// - every superseded turn (its opening, its work, its answer: turn_range.js's
|
|
@@ -28,14 +28,18 @@ export function createInUseWatch({ stage, watching, onTick, onDue }) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function inUse(nowMs) {
|
|
31
|
-
|
|
31
|
+
// Through shadow roots (a chi-ui widget, inline_widgets.js): the host
|
|
32
|
+
// is document.activeElement, the focused element is inside it.
|
|
33
|
+
let active = document.activeElement;
|
|
34
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
32
35
|
const sel = window.getSelection();
|
|
33
36
|
return inUseFrom({
|
|
34
37
|
pointerIn,
|
|
35
38
|
lastTouchMs: lastTouch,
|
|
36
39
|
// A clicked button keeps the focus in Chrome: only keyboard focus and
|
|
37
40
|
// a field being typed in count.
|
|
38
|
-
focusInside: !!active && stage.contains(
|
|
41
|
+
focusInside: !!active && stage.contains(document.activeElement)
|
|
42
|
+
&& (/^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName) || active.matches(":focus-visible")),
|
|
39
43
|
selectionInside: !!sel && !sel.isCollapsed && stage.contains(sel.anchorNode),
|
|
40
44
|
nowMs,
|
|
41
45
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The texts the page treats as text it wrote out of a model's words: the
|
|
2
2
|
// answers and the closed steps' texts. Cards, question replies, the asking
|
|
3
3
|
// slot and delegate reports aren't among them. markdown_place.js links refs on
|
|
4
|
-
// these;
|
|
4
|
+
// these; block_watch.js finds blocks to draw in these. A module of its own: static JS is cached
|
|
5
5
|
// at unversioned URLs, and a new export on an old module would blank the page.
|
|
6
6
|
export const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
|