samagotchi 0.57.0 → 0.59.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 +56 -1
- data/docs/architecture.md +3 -3
- data/docs/cli.md +5 -4
- data/docs/configuration.md +40 -23
- data/docs/hooks.md +2 -2
- data/docs/internals/background-tasks.md +7 -0
- data/docs/internals/engine.md +5 -5
- data/docs/internals/prompt-caching.md +2 -2
- data/docs/internals/tool-guardrails.md +2 -1
- data/docs/plugins.md +26 -6
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +6 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +5 -4
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +169 -16
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +11 -5
- data/lib/samagotchi/engine.rb +28 -3
- data/lib/samagotchi/host_config.rb +3 -1
- data/lib/samagotchi/host_registry.rb +2 -1
- data/lib/samagotchi/model_profile.rb +14 -3
- data/lib/samagotchi/model_settings.rb +4 -2
- data/lib/samagotchi/process_group.rb +76 -3
- data/lib/samagotchi/redacted_inspect.rb +21 -6
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_commands.rb +19 -6
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/steer_cut_after.rb +69 -0
- data/lib/samagotchi/system_prompt.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +9 -5
- data/lib/samagotchi/terminal_ui.rb +15 -6
- data/lib/samagotchi/tool_activity.rb +18 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/execute.rb +318 -70
- data/lib/samagotchi/tools/task_get.rb +2 -0
- data/lib/samagotchi/tools/task_runtime.rb +96 -13
- data/lib/samagotchi/tools/task_wait.rb +23 -5
- data/lib/samagotchi/utf8_default.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +8 -2
- data/lib/samagotchi/web/public/app.js +7 -6
- data/lib/samagotchi/web/public/index.html +20 -7
- data/lib/samagotchi/web/public/read_view.js +3 -1
- data/lib/samagotchi/web/public/ref_menu.js +188 -0
- data/lib/samagotchi/web/public/ref_scan.js +11 -1
- data/lib/samagotchi/web/public/refs.js +271 -51
- data/lib/samagotchi/web/public/stage_view.js +21 -0
- data/lib/samagotchi/web/public/task_stop.js +25 -7
- data/lib/samagotchi/worker.rb +2 -2
- metadata +3 -1
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
// a kind and a target: today a file tool's row title (the server's FileRef,
|
|
3
3
|
// {kind: "file", path, line?, end_line?}), that row's diff lines and a
|
|
4
4
|
// read row's numbered lines (the same file at a line), and an edit/write
|
|
5
|
-
// approval card's paths and its dry run's diff lines
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// approval card's paths and its dry run's diff lines, and a `path:line` in
|
|
6
|
+
// an answer or a step's text (ref_scan.js; a link whose text is one too).
|
|
7
|
+
// The DOM carries data-ref-kind and the target's attributes, never what a
|
|
8
|
+
// click does: those are the ref's targets, kept per kind in REF_ACTIONS.
|
|
9
|
+
// A plain click runs the first one available (the main target); a ref
|
|
10
|
+
// with more shows a ▾ on hover that opens a menu of all of them
|
|
11
|
+
// (ref_menu.js), and a tap opens it. A ref with a target is a tab stop
|
|
12
|
+
// (syncRefTabs): Enter is its click, ↓ its menu. A ref with none (a phone's row title:
|
|
13
|
+
// no editor, and the row is the ref's own) stays plain text; refs.js marks
|
|
14
|
+
// a hovered or focused ref with its main target (data-ref-main), which the
|
|
15
|
+
// hover styling keys on.
|
|
11
16
|
|
|
12
17
|
import { escapeHtml } from "./format.js";
|
|
18
|
+
import { copyText } from "./copy.js";
|
|
19
|
+
import { createRefMenu } from "./ref_menu.js";
|
|
13
20
|
|
|
14
|
-
// The data attributes a ref's element carries
|
|
21
|
+
// The data attributes a ref's element carries (its tab stop comes from
|
|
22
|
+
// syncRefTabs, for the viewer).
|
|
15
23
|
export function refAttrs(ref) {
|
|
16
24
|
if (!ref?.kind) return "";
|
|
17
25
|
const attr = (name, value) => (value ? ` data-ref-${name}="${escapeHtml(String(value))}"` : "");
|
|
@@ -27,29 +35,59 @@ export function editorUrl(template, path, line) {
|
|
|
27
35
|
return template.replaceAll("{path}", encoded).replaceAll("{line}", String(line || 1));
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
//
|
|
31
|
-
// click's
|
|
32
|
-
//
|
|
38
|
+
// A ref's targets, per kind, in order: the first available one is a plain
|
|
39
|
+
// click's (the main target), all of them are the menu's. Each says when
|
|
40
|
+
// it's available for +viewer+ (what this page's viewer can do: {editor,
|
|
41
|
+
// editorUrl}, the server's capabilities.editor and editor_url) and
|
|
42
|
+
// +target+ (refTarget's). A secondary one (copy) joins a ref that has
|
|
43
|
+
// another, never alone. A line (a diff or read line) has its click only.
|
|
33
44
|
export const REF_ACTIONS = Object.freeze({
|
|
34
45
|
file: Object.freeze([
|
|
46
|
+
// A link whose text is the ref (github-pr's PR diff): its href stays
|
|
47
|
+
// the plain click's, which the browser follows.
|
|
48
|
+
Object.freeze({
|
|
49
|
+
id: "link",
|
|
50
|
+
label: (target) => (/\/pull\/\d+\/files\b/.test(target.href) ? "PR diff" : "Open link"),
|
|
51
|
+
available: (_viewer, target) => !!target.href,
|
|
52
|
+
run: (target, _viewer, env) => env.openLink(target.href),
|
|
53
|
+
}),
|
|
35
54
|
Object.freeze({
|
|
36
55
|
id: "editor",
|
|
56
|
+
label: () => "Open in editor",
|
|
37
57
|
available: (viewer) => viewer?.editor === true && !!viewer.editorUrl,
|
|
38
58
|
url: (target, viewer) => editorUrl(viewer.editorUrl, target.path, target.line),
|
|
59
|
+
run(target, viewer, env) {
|
|
60
|
+
const url = this.url(target, viewer);
|
|
61
|
+
if (url) env.open(url);
|
|
62
|
+
},
|
|
39
63
|
// The hover hint: it opens the file as it is now.
|
|
40
64
|
hint: (target) => `Open the current file in your editor${target.line ? ` at line ${target.line}` : ""}`,
|
|
41
65
|
}),
|
|
66
|
+
// The row that touched the file, in the turn: not for a row's own
|
|
67
|
+
// title or lines (they are in the turn already).
|
|
68
|
+
Object.freeze({
|
|
69
|
+
id: "show",
|
|
70
|
+
label: () => "Show in turn",
|
|
71
|
+
available: (_viewer, target) => !target.isLine && !target.owner?.matches?.(".activity-row") && !!turnRowFor(target),
|
|
72
|
+
run: (target, _viewer, env) => env.show(target),
|
|
73
|
+
hint: () => "Show the row that touched it in the turn",
|
|
74
|
+
}),
|
|
75
|
+
Object.freeze({
|
|
76
|
+
id: "copy",
|
|
77
|
+
label: () => "Copy path",
|
|
78
|
+
secondary: true,
|
|
79
|
+
available: (_viewer, target) => !target.isLine && !!target.path,
|
|
80
|
+
run: (target, _viewer, env) => env.copy(target.path),
|
|
81
|
+
}),
|
|
42
82
|
]),
|
|
43
83
|
});
|
|
44
84
|
|
|
45
|
-
// The
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export function availableKinds(viewer, actions = REF_ACTIONS) {
|
|
52
|
-
return Object.keys(actions).filter((kind) => refAction(kind, viewer, actions));
|
|
85
|
+
// The targets +viewer+ has for +target+ (refTarget's), in order; none
|
|
86
|
+
// when only a secondary one is.
|
|
87
|
+
export function refActions(target, viewer, actions = REF_ACTIONS) {
|
|
88
|
+
if (!target) return [];
|
|
89
|
+
const found = (actions[target.kind] || []).filter((action) => action.available(viewer, target));
|
|
90
|
+
return found.some((action) => !action.secondary) ? found : [];
|
|
53
91
|
}
|
|
54
92
|
|
|
55
93
|
// What owns a ref and its diff lines: a tool row, or an edit's approval
|
|
@@ -59,23 +97,25 @@ const OWNERS = ".activity-row, .bubble.approval";
|
|
|
59
97
|
// A line of the owner's file: a diff line, or a read row's numbered line.
|
|
60
98
|
const LINES = ".diff-line[data-line], .read-line[data-line]";
|
|
61
99
|
|
|
62
|
-
// What a click on +el+ points at: {kind, path, line, owner
|
|
63
|
-
// title (or an approval card's path) opens its file
|
|
64
|
-
// named (an edit without any at its first changed
|
|
65
|
-
// hunk header) or a read row's numbered line, in
|
|
66
|
-
// ref, opens that file at that line.
|
|
100
|
+
// What a click on +el+ points at: {kind, path, line, owner, el, isLine,
|
|
101
|
+
// href} or null. A row title (or an approval card's path) opens its file
|
|
102
|
+
// at the lines the call named (an edit without any at its first changed
|
|
103
|
+
// line); a diff line (or a hunk header) or a read row's numbered line, in
|
|
104
|
+
// a row or a card with a ref, opens that file at that line. +el+ is the
|
|
105
|
+
// ref's element (the line for a line), +href+ a link ref's.
|
|
67
106
|
// A diff line elsewhere is none.
|
|
68
107
|
export function refTarget(el) {
|
|
69
108
|
const title = el?.closest?.("[data-ref-kind]");
|
|
70
109
|
if (title) {
|
|
71
110
|
const owner = title.closest(OWNERS);
|
|
72
|
-
|
|
111
|
+
const href = title.matches("a[href]") ? title.getAttribute("href") : null;
|
|
112
|
+
return { ...targetOf(title), line: Number(title.dataset.refLine) || firstChange(owner), owner: owner || title, el: title, isLine: false, href };
|
|
73
113
|
}
|
|
74
114
|
const diffLine = el?.closest?.(LINES);
|
|
75
115
|
const owner = diffLine?.closest(OWNERS);
|
|
76
116
|
const ref = owner?.querySelector("[data-ref-kind]");
|
|
77
117
|
if (!ref) return null;
|
|
78
|
-
return { ...targetOf(ref), line: Number(diffLine.dataset.line), owner };
|
|
118
|
+
return { ...targetOf(ref), line: Number(diffLine.dataset.line), owner, el: diffLine, isLine: true, href: null };
|
|
79
119
|
}
|
|
80
120
|
|
|
81
121
|
function targetOf(el) {
|
|
@@ -89,6 +129,99 @@ function firstChange(row) {
|
|
|
89
129
|
return line ? Number(line.dataset.line) : null;
|
|
90
130
|
}
|
|
91
131
|
|
|
132
|
+
// The tool row that touched +target+'s file, for "show in turn", or null:
|
|
133
|
+
// a row with a ref to the same file that isn't the ref's own. Looked for
|
|
134
|
+
// in the ref's own step (a step's text comes before its rows), then its
|
|
135
|
+
// own turn (turnBlockOf), then the rows before it, then the nearest after
|
|
136
|
+
// it; in the first of those that has any, the last one showing the ref's
|
|
137
|
+
// line, else the last one (after it: the nearest).
|
|
138
|
+
export function turnRowFor(target) {
|
|
139
|
+
const el = target?.el;
|
|
140
|
+
if (!el?.ownerDocument || !target.path) return null;
|
|
141
|
+
const rows = [...el.ownerDocument.querySelectorAll(".activity-row")].filter((row) => {
|
|
142
|
+
if (row.contains(el)) return false;
|
|
143
|
+
const ref = row.querySelector("[data-ref-kind]");
|
|
144
|
+
return ref?.dataset.refKind === target.kind && ref.dataset.refPath === target.path;
|
|
145
|
+
});
|
|
146
|
+
const step = el.closest("details.gen");
|
|
147
|
+
const turn = turnBlockOf(el);
|
|
148
|
+
const before = rows.filter((row) => precedes(row, el));
|
|
149
|
+
const pools = [
|
|
150
|
+
step ? rows.filter((row) => step.contains(row)) : [],
|
|
151
|
+
turn ? rows.filter((row) => turn.contains(row)) : [],
|
|
152
|
+
before,
|
|
153
|
+
rows.filter((row) => !before.includes(row)).reverse(),
|
|
154
|
+
];
|
|
155
|
+
const pool = pools.find((found) => found.length);
|
|
156
|
+
if (!pool) return null;
|
|
157
|
+
const showing = target.line ? pool.filter((row) => lineIn(row, target.line)) : [];
|
|
158
|
+
return showing.at(-1) || pool.at(-1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// What holds +el+'s turn's rows: the steps block it is in, the stage (a
|
|
162
|
+
// live turn: its answer comes before its cloud), else the nearest steps
|
|
163
|
+
// block before it with no prompt between them (an answer, a card, in the
|
|
164
|
+
// history). null when there is none.
|
|
165
|
+
function turnBlockOf(el) {
|
|
166
|
+
const own = el.closest(".turn-work, .turn-stage");
|
|
167
|
+
if (own) return own;
|
|
168
|
+
const doc = el.ownerDocument;
|
|
169
|
+
const block = [...doc.querySelectorAll(".turn-work")].filter((b) => precedes(b, el)).at(-1);
|
|
170
|
+
if (!block) return null;
|
|
171
|
+
const prompt = [...doc.querySelectorAll(".bubble.user")].some((u) => precedes(block, u) && precedes(u, el));
|
|
172
|
+
return prompt ? null : block;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// +a+ comes before +b+ in the page (and doesn't hold it).
|
|
176
|
+
function precedes(a, b) {
|
|
177
|
+
return !a.contains(b) && !!(a.compareDocumentPosition(b) & 4 /* Node.DOCUMENT_POSITION_FOLLOWING */);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// +row+'s line +line+: an added or context diff line first (a removed one
|
|
181
|
+
// shares its number), then a read line; null when it doesn't show it.
|
|
182
|
+
function lineIn(row, line) {
|
|
183
|
+
for (const selector of [".diff-line.diff-add", ".diff-line.diff-ctx", ".read-line", ".diff-line"]) {
|
|
184
|
+
const found = row.querySelector(`${selector}[data-line="${Number(line)}"]`);
|
|
185
|
+
if (found) return found;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// "Show in turn": opens what hides the row that touched +target+'s file
|
|
191
|
+
// (the turn's steps, its step), its diff (or the read that shows the
|
|
192
|
+
// line), scrolls to the line (else the row) and flashes both. A view that
|
|
193
|
+
// hides its rows its own way (the stage's cloud) opens on the row's
|
|
194
|
+
// ref:reveal event, which bubbles. The row, or null when there is none.
|
|
195
|
+
export function showInTurn(target, { behavior = smoothScroll() } = {}) {
|
|
196
|
+
const row = turnRowFor(target);
|
|
197
|
+
if (!row) return null;
|
|
198
|
+
const view = row.ownerDocument.defaultView || globalThis;
|
|
199
|
+
row.dispatchEvent(new view.CustomEvent("ref:reveal", { bubbles: true }));
|
|
200
|
+
const line = target.line ? lineIn(row, target.line) : null;
|
|
201
|
+
const fold = line?.closest("details.activity-diff, details.activity-read") || row.querySelector("details.activity-diff");
|
|
202
|
+
for (let details = (line || fold || row).closest("details"); details; details = details.parentElement?.closest("details")) {
|
|
203
|
+
details.open = true;
|
|
204
|
+
}
|
|
205
|
+
if (fold) fold.open = true;
|
|
206
|
+
(line || row).scrollIntoView?.({ block: "center", behavior });
|
|
207
|
+
for (const el of [row, line].filter(Boolean)) flash(el);
|
|
208
|
+
return row;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const FLASH_MS = 1600;
|
|
212
|
+
|
|
213
|
+
function flash(el) {
|
|
214
|
+
el.classList.remove("ref-flash");
|
|
215
|
+
void el.offsetWidth; // restarts the animation on a second show
|
|
216
|
+
el.classList.add("ref-flash");
|
|
217
|
+
clearTimeout(el._refFlash);
|
|
218
|
+
el._refFlash = setTimeout(() => el.classList.remove("ref-flash"), FLASH_MS);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function smoothScroll() {
|
|
222
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth";
|
|
223
|
+
}
|
|
224
|
+
|
|
92
225
|
// A drag that selected text in the clicked ref's row isn't a click on the
|
|
93
226
|
// ref. A selection elsewhere on the page (or a collapsed one) doesn't count.
|
|
94
227
|
function selectingIn(owner, selection) {
|
|
@@ -102,42 +235,129 @@ function defaultOpen(url) {
|
|
|
102
235
|
else window.location.href = url;
|
|
103
236
|
}
|
|
104
237
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
238
|
+
// A link ref's href from the menu: a new tab, as the answer's links open.
|
|
239
|
+
function defaultOpenLink(href) {
|
|
240
|
+
window.open(href, "_blank", "noopener");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// One delegated listener set on +root+ for every ref on the page.
|
|
244
|
+
// +viewer+() is read per click, key and hover, so a capabilities change
|
|
245
|
+
// (another session, a reload) is seen. The targets run through: +open+(url)
|
|
246
|
+
// (the editor's URL), +openLink+(href), +show+(target) and +copy+(text).
|
|
247
|
+
// +menu+ is the ▾ and its menu (ref_menu.js).
|
|
248
|
+
export function installRefClicks(root, viewer, {
|
|
249
|
+
open = defaultOpen, openLink = defaultOpenLink, show = (target) => showInTurn(target), copy = copyText,
|
|
250
|
+
selection = () => window.getSelection?.(), menu = createRefMenu(root.ownerDocument),
|
|
251
|
+
} = {}) {
|
|
252
|
+
const env = { open, openLink, show, copy };
|
|
253
|
+
const run = (action, target) => action.run(target, viewer(), env);
|
|
254
|
+
const items = (actions, target) => actions.map((action) => ({ id: action.id, label: action.label(target), run: () => run(action, target) }));
|
|
255
|
+
const openMenu = (target, actions, options) => menu.open(target.el, items(actions, target), options);
|
|
256
|
+
// The last pointer's type: a tap opens the menu, a mouse click runs the
|
|
257
|
+
// main target.
|
|
258
|
+
let pointer = "mouse";
|
|
259
|
+
root.addEventListener("pointerdown", (e) => { pointer = e.pointerType || "mouse"; }, true);
|
|
260
|
+
// A key press: the keyboard leads now (Enter on a link after a tap is
|
|
261
|
+
// the link's own click); Tab works out the tab stops before it moves.
|
|
262
|
+
root.addEventListener("keydown", (e) => {
|
|
263
|
+
pointer = "keyboard";
|
|
264
|
+
if (e.key === "Tab") syncRefTabs(root, viewer());
|
|
265
|
+
}, true);
|
|
266
|
+
|
|
109
267
|
root.addEventListener("click", (e) => {
|
|
110
268
|
if (e.button !== 0 || e.defaultPrevented) return;
|
|
111
269
|
const target = refTarget(e.target);
|
|
112
270
|
if (!target) return;
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
271
|
+
const actions = refActions(target, viewer());
|
|
272
|
+
if (!actions.length) return;
|
|
115
273
|
if (selectingIn(target.owner, selection())) return;
|
|
116
|
-
|
|
117
|
-
|
|
274
|
+
if (pointer === "touch" && !target.isLine && actions.length > 1) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
openMenu(target, actions, { focus: false });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// A link's own click: the browser follows it.
|
|
281
|
+
if (actions[0].id === "link") return;
|
|
118
282
|
e.preventDefault();
|
|
119
283
|
e.stopPropagation();
|
|
120
|
-
|
|
284
|
+
run(actions[0], target);
|
|
121
285
|
});
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
root.addEventListener("
|
|
286
|
+
|
|
287
|
+
// A focused ref: Enter is its main target (a link's Enter is its own
|
|
288
|
+
// click), ↓ opens the menu with the first target focused, Esc hides the ▾.
|
|
289
|
+
root.addEventListener("keydown", (e) => {
|
|
290
|
+
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
291
|
+
const el = e.target;
|
|
292
|
+
if (!el?.matches?.("[data-ref-kind]")) return;
|
|
293
|
+
if (e.key === "Escape") menu.hideMore();
|
|
294
|
+
if (e.key !== "Enter" && e.key !== "ArrowDown") return;
|
|
295
|
+
const target = refTarget(el);
|
|
296
|
+
const actions = refActions(target, viewer());
|
|
297
|
+
if (!actions.length) return;
|
|
298
|
+
if (e.key === "Enter") {
|
|
299
|
+
if (actions[0].id === "link") return;
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
run(actions[0], target);
|
|
302
|
+
} else if (actions.length > 1) {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
openMenu(target, actions, { focus: true });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// On hover or focus: the ref's main target on it (data-ref-main, for the
|
|
309
|
+
// styling), its hover hint, and the ▾ when it has more than one (not
|
|
310
|
+
// after a tap, which opens the menu itself).
|
|
311
|
+
const mark = (e) => {
|
|
126
312
|
const target = refTarget(e.target);
|
|
127
313
|
if (!target) return;
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
314
|
+
const actions = refActions(target, viewer());
|
|
315
|
+
const el = target.el;
|
|
316
|
+
if (actions.length) el.dataset.refMain = actions[0].id;
|
|
317
|
+
else delete el.dataset.refMain;
|
|
318
|
+
if (!target.isLine) setRefRole(el, actions);
|
|
319
|
+
hint(el, target, actions[0]);
|
|
320
|
+
if (!target.isLine && actions.length > 1 && pointer !== "touch") menu.showMore(el, () => openMenu(refTarget(el), refActions(refTarget(el), viewer()), { focus: true }));
|
|
321
|
+
};
|
|
322
|
+
root.addEventListener("mouseover", mark);
|
|
323
|
+
root.addEventListener("focusin", mark);
|
|
324
|
+
const leave = (e) => {
|
|
325
|
+
const el = e.target?.closest?.("[data-ref-kind]");
|
|
326
|
+
if (el && !el.contains(e.relatedTarget)) menu.hideMoreSoon(el);
|
|
327
|
+
};
|
|
328
|
+
root.addEventListener("mouseout", leave);
|
|
329
|
+
root.addEventListener("focusout", leave);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// The tab stops under +root+ for +viewer+: a ref is one (a button, with
|
|
333
|
+
// aria-haspopup when it has a menu) only while it has a target, so a
|
|
334
|
+
// phone's or an editorless viewer's row titles aren't. Worked out before a
|
|
335
|
+
// Tab moves the focus and on a ref's hover or focus; a line never is one.
|
|
336
|
+
export function syncRefTabs(root, viewer) {
|
|
337
|
+
for (const el of root.querySelectorAll("[data-ref-kind]")) setRefRole(el, refActions(refTarget(el), viewer));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function setRefRole(el, actions) {
|
|
341
|
+
if (actions.length > 1) el.setAttribute("aria-haspopup", "menu");
|
|
342
|
+
else el.removeAttribute("aria-haspopup");
|
|
343
|
+
// A link is focusable and a link already.
|
|
344
|
+
if (el.matches("a[href]")) return;
|
|
345
|
+
if (actions.length) {
|
|
346
|
+
el.tabIndex = 0;
|
|
347
|
+
el.setAttribute("role", "button");
|
|
348
|
+
} else {
|
|
349
|
+
el.removeAttribute("tabindex");
|
|
350
|
+
el.removeAttribute("role");
|
|
351
|
+
}
|
|
137
352
|
}
|
|
138
353
|
|
|
139
|
-
// The
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
354
|
+
// The hover hint, on the ref's own element, worked out on each hover (the
|
|
355
|
+
// viewer can change): its own title (a row title's hover) first, then the
|
|
356
|
+
// main target's hint when it has one.
|
|
357
|
+
function hint(el, target, action) {
|
|
358
|
+
if (el.dataset.refTitle === undefined) el.dataset.refTitle = el.getAttribute("title") || "";
|
|
359
|
+
const own = el.dataset.refTitle;
|
|
360
|
+
const title = [own, action?.hint ? action.hint(target) : ""].filter(Boolean).join("\n");
|
|
361
|
+
if (title) el.setAttribute("title", title);
|
|
362
|
+
else el.removeAttribute("title");
|
|
143
363
|
}
|
|
@@ -106,6 +106,8 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
106
106
|
let flash = null; // { kind, text, until } a notice or nudge in the trail
|
|
107
107
|
let collapsed = readCollapsed();
|
|
108
108
|
let cloudOpen = false;
|
|
109
|
+
// A ref's "show in turn" opened a collapsed stage (not kept).
|
|
110
|
+
let revealed = false;
|
|
109
111
|
let rafPending = false;
|
|
110
112
|
let foldTimer = null;
|
|
111
113
|
let watchTimer = null;
|
|
@@ -326,6 +328,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
326
328
|
|
|
327
329
|
function setCollapsed(value) {
|
|
328
330
|
collapsed = value;
|
|
331
|
+
revealed = false;
|
|
329
332
|
try { localStorage.setItem(COLLAPSE_KEY, value ? "1" : "0"); } catch (_) {}
|
|
330
333
|
applyCollapsed();
|
|
331
334
|
}
|
|
@@ -370,6 +373,19 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
370
373
|
else setCloud(!cloudOpen);
|
|
371
374
|
});
|
|
372
375
|
headline.addEventListener("click", peek);
|
|
376
|
+
// A ref's "show in turn" (refs.js) names a row in the cloud: open the
|
|
377
|
+
// stage and the cloud to it, as a hand toggle would.
|
|
378
|
+
// A collapsed stage opens for this turn only: the kept choice
|
|
379
|
+
// (chi_stage_collapsed) is left alone and comes back at the release.
|
|
380
|
+
stage.addEventListener("ref:reveal", (e) => {
|
|
381
|
+
if (!cloudBody.contains(e.target)) return;
|
|
382
|
+
if (collapsed) {
|
|
383
|
+
collapsed = false;
|
|
384
|
+
revealed = true;
|
|
385
|
+
applyCollapsed();
|
|
386
|
+
}
|
|
387
|
+
setCloud(true);
|
|
388
|
+
});
|
|
373
389
|
headline.addEventListener("keydown", (e) => {
|
|
374
390
|
if (e.key === "Enter" || e.key === " ") {
|
|
375
391
|
e.preventDefault();
|
|
@@ -491,6 +507,11 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
491
507
|
settled = true;
|
|
492
508
|
view.reset();
|
|
493
509
|
emptySlots();
|
|
510
|
+
if (revealed) {
|
|
511
|
+
revealed = false;
|
|
512
|
+
collapsed = readCollapsed();
|
|
513
|
+
applyCollapsed();
|
|
514
|
+
}
|
|
494
515
|
}
|
|
495
516
|
|
|
496
517
|
// Everything the stage holds, into the history's end in the turn view's
|
|
@@ -22,10 +22,30 @@ export function stopTarget(row, features) {
|
|
|
22
22
|
// A task_wait row's title without its wait ("npm test · up to 600s").
|
|
23
23
|
const WAIT_SUFFIX = / · up to \d+s$/;
|
|
24
24
|
|
|
25
|
-
// The
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// The first line of an execute that was still running at its limit and
|
|
26
|
+
// went on as a task (Tools::Execute::HANDED_OVER), then its task_id line.
|
|
27
|
+
// Anchored at the start, an optional "ran as: …" line and "[execute]" lead
|
|
28
|
+
// before it: a "task_id:" line anywhere else is the command's own output.
|
|
29
|
+
// The twin of TaskRuntime.handed_over_id.
|
|
30
|
+
const HANDED_OVER = /^(?:ran as: [^\n]*\n)?(?:\[execute\]\n)?Still running at the \d+s limit:[^\n]*\ntask_id: (\S+)/;
|
|
31
|
+
|
|
32
|
+
// The task id an execute row's output hands over, else null.
|
|
33
|
+
export function handedOverId(output) {
|
|
34
|
+
return HANDED_OVER.exec(output || "")?.[1] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The task id a row of this turn started: a task_create's, or an execute's
|
|
38
|
+
// hand-over (handedOverId); else null.
|
|
39
|
+
function startedId(row) {
|
|
40
|
+
if (row.tool === "execute") return handedOverId(row.output);
|
|
41
|
+
if (row.tool !== "task_create") return null;
|
|
42
|
+
return /(?:^|\n)task_id: (\S+)/.exec(row.output || "")?.[1] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The command a task_create or a handed-over execute of this turn started
|
|
46
|
+
// +taskId+ with (its row's view), else a task_wait row's title for it (the
|
|
47
|
+
// server titles one by its task's command, also for a task from an earlier
|
|
48
|
+
// turn: ToolActivity.task_title), or null.
|
|
29
49
|
export function taskCommand(gens, taskId) {
|
|
30
50
|
let titled = null;
|
|
31
51
|
for (const gen of gens || []) {
|
|
@@ -33,9 +53,7 @@ export function taskCommand(gens, taskId) {
|
|
|
33
53
|
if (row.tool === "task_wait" && row.view?.task_id === taskId && row.title) {
|
|
34
54
|
titled ||= row.title.replace(WAIT_SUFFIX, "");
|
|
35
55
|
}
|
|
36
|
-
if (row
|
|
37
|
-
const id = /(?:^|\n)task_id: (\S+)/.exec(row.output || "")?.[1];
|
|
38
|
-
if (id === taskId) return row.view?.command || null;
|
|
56
|
+
if (startedId(row) === taskId) return row.view?.command || null;
|
|
39
57
|
}
|
|
40
58
|
}
|
|
41
59
|
return titled;
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -219,8 +219,8 @@ module Samagotchi
|
|
|
219
219
|
@bridge.queue_command(line, client_id: client_id, after_file: after_file)
|
|
220
220
|
})
|
|
221
221
|
# /model's default is the config's, as in the REPL; the Engine started
|
|
222
|
-
# on the session's model.
|
|
223
|
-
@default_model = ModelProfile.
|
|
222
|
+
# on the session's model. nil when there is no default.model.
|
|
223
|
+
@default_model = ModelProfile.default_model_name
|
|
224
224
|
@commands = SessionCommands.new(engine: @engine, turn_flow: @turn_flow,
|
|
225
225
|
default_model: @default_model, registry: @engine.command_registry,
|
|
226
226
|
save: ->(session) { session.save(state_dir: @state_dir) })
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.59.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -430,6 +430,7 @@ files:
|
|
|
430
430
|
- lib/samagotchi/setting_explained.rb
|
|
431
431
|
- lib/samagotchi/steer.rb
|
|
432
432
|
- lib/samagotchi/steer_cut.rb
|
|
433
|
+
- lib/samagotchi/steer_cut_after.rb
|
|
433
434
|
- lib/samagotchi/system_prompt.rb
|
|
434
435
|
- lib/samagotchi/terminal_ui.rb
|
|
435
436
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
@@ -562,6 +563,7 @@ files:
|
|
|
562
563
|
- lib/samagotchi/web/public/question_cards.js
|
|
563
564
|
- lib/samagotchi/web/public/read_view.js
|
|
564
565
|
- lib/samagotchi/web/public/reasoning_history_chip.js
|
|
566
|
+
- lib/samagotchi/web/public/ref_menu.js
|
|
565
567
|
- lib/samagotchi/web/public/ref_scan.js
|
|
566
568
|
- lib/samagotchi/web/public/refs.js
|
|
567
569
|
- lib/samagotchi/web/public/restore.js
|