samagotchi 0.54.0 → 0.55.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 +21 -1
- data/bin/chi +3 -2
- data/docs/configuration.md +29 -0
- data/docs/internals/context-telemetry.md +23 -5
- data/docs/internals/engine.md +40 -18
- data/docs/sessions.md +2 -2
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +2 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +25 -2
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +33 -18
- data/lib/samagotchi/web/public/app.js +237 -1133
- data/lib/samagotchi/web/public/commands.js +236 -0
- data/lib/samagotchi/web/public/composer.js +101 -0
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +5 -0
- data/lib/samagotchi/web/public/info_bar.js +78 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/question_cards.js +2 -1
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +52 -0
- data/lib/samagotchi/web/public/stream_handlers.js +324 -0
- data/lib/samagotchi/web/public/thinking_chip.js +6 -7
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +5 -1
- data/lib/samagotchi/web/server.rb +4 -4
- metadata +11 -2
- data/lib/samagotchi/web/public/stable_html.js +0 -18
|
@@ -37,13 +37,12 @@ export function chipModel(summary, { kind = SESSION } = {}) {
|
|
|
37
37
|
return { text: `think ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// The session bar's chip
|
|
41
|
-
//
|
|
42
|
-
export function
|
|
40
|
+
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
41
|
+
// without a summary.
|
|
42
|
+
export function chipTemplate(summary) {
|
|
43
43
|
const chip = chipModel(summary);
|
|
44
|
-
if (!chip) return
|
|
45
|
-
return `<button type="button" class
|
|
46
|
-
`data-think-chip aria-haspopup="dialog">${escapeHtml(chip.text)}</button>`;
|
|
44
|
+
if (!chip) return nothing;
|
|
45
|
+
return html`<button type="button" class=${`model think-chip${chip.own ? " own" : ""}`} title=${chip.title} data-think-chip aria-haspopup="dialog">${chip.text}</button>`;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
// The /thinking line for a select value ("default" unsets the session's
|
|
@@ -81,7 +80,7 @@ export function formTemplate(summary, { running = false, kind = SESSION, choice
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
// The chip's popover, as createLlmContextControl's: el is where the chip
|
|
84
|
-
// is drawn (clicks are delegated: #infoText
|
|
83
|
+
// is drawn (clicks are delegated: #infoText, info_bar.js);
|
|
85
84
|
// summary(): the open session's thinking; session(): its id; running():
|
|
86
85
|
// a turn runs; run(line): sends the command. The start page's (kind
|
|
87
86
|
// NEW_CHAT): summary() is the picked model's, choice() the pending level
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Pure helpers for the Bridge's turn events (no DOM), so app.js's handlers
|
|
2
2
|
// stay thin and the logic runs under `node --test`.
|
|
3
3
|
|
|
4
|
+
import { ACTIVITY_FIELDS, EVENT_FIELDS, pickFields } from "./activity.js";
|
|
5
|
+
|
|
4
6
|
// The answer text of a :turn_completed event. `turn_summary.output` is the
|
|
5
7
|
// contract; `result` is the KernelLoop result, which reaches the wire as its
|
|
6
8
|
// string form (older servers, other clients' fixtures).
|
|
@@ -189,6 +191,11 @@ export function llmContextHover({ groups = [], freed_tokens: freed, tail_tokens:
|
|
|
189
191
|
return lines.join("\n");
|
|
190
192
|
}
|
|
191
193
|
|
|
194
|
+
// A ✂ row's notice data (turnDom.notice, placeTurnNotice).
|
|
195
|
+
export function llmContextNotice(data) {
|
|
196
|
+
return { line: llmContextLine(data), cls: "llm-context", title: llmContextHover(data) };
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
// The one muted line for a turn that ended with no answer (turn_completed's
|
|
193
200
|
// turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
|
|
194
201
|
// it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
|
|
@@ -229,257 +236,6 @@ export function initWaitLine({ tasks } = {}) {
|
|
|
229
236
|
return `waiting for ${labels.length ? labels.join(" · ") : "plugins"}…`;
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
// Whether a composer line in an open session is one of its commands
|
|
233
|
-
// (+commands+: the session GET's listing), as the terminals decide
|
|
234
|
-
// (Commands::Registry#lookup): a "!" line with something after the "!", or
|
|
235
|
-
// a listed name alone or followed by a space; a terminal's own (/stats,
|
|
236
|
-
// /exit) too, which the page answers itself (webLocalReply). An unknown
|
|
237
|
-
// "/word" (a path, a typo) goes to the model as a prompt, as in a terminal.
|
|
238
|
-
export function sessionCommandLine(text, commands) {
|
|
239
|
-
const line = String(text || "").trim();
|
|
240
|
-
if (/^!\s*\S/.test(line)) return true;
|
|
241
|
-
if (!line.startsWith("/")) return false;
|
|
242
|
-
if (webLocalReply(line)) return true;
|
|
243
|
-
return (Array.isArray(commands) ? commands : [])
|
|
244
|
-
.some((c) => c && typeof c.name === "string" && (line === c.name || line.startsWith(`${c.name} `)));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// /cut TEXT and /queue TEXT: the UIs' own words for a delivery (listed
|
|
248
|
-
// for Tab and /help, never run as commands; TerminalUI::Formatting's
|
|
249
|
-
// STEER_CHOICE: the word, then any whitespace and the text). During a
|
|
250
|
-
// turn /cut TEXT is the text sent as a cut (POST /turn, delivery "cut"),
|
|
251
|
-
// as chi send --cut does; /queue isn't in the web yet (the page says so,
|
|
252
|
-
// as the attached TUI does). With no running turn both are the text as a
|
|
253
|
-
// normal turn. The bare word gets the usage line.
|
|
254
|
-
// @return {{send: string, delivery?: string} | {reply: string} | null}
|
|
255
|
-
// null: not a delivery line
|
|
256
|
-
export const STEER_CHOICE_USAGE = "(/cut and /queue need a message: /cut TEXT, /queue TEXT)";
|
|
257
|
-
export const QUEUE_UNAVAILABLE_WEB = "(/queue isn't available in the web yet; a plain message goes in at the running turn's next step, and /cut TEXT cuts in)";
|
|
258
|
-
const STEER_CHOICE = /^\/(cut|queue)(?:\s+([\s\S]*))?$/;
|
|
259
|
-
|
|
260
|
-
export function steerChoiceRoute(text, { running = false } = {}) {
|
|
261
|
-
const match = STEER_CHOICE.exec(String(text || "").trim());
|
|
262
|
-
if (!match) return null;
|
|
263
|
-
const message = (match[2] || "").trim();
|
|
264
|
-
if (!message) return { reply: STEER_CHOICE_USAGE };
|
|
265
|
-
if (!running) return { send: message };
|
|
266
|
-
if (match[1] === "queue") return { reply: QUEUE_UNAVAILABLE_WEB };
|
|
267
|
-
return { send: message, delivery: "cut" };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// A line that is one word: a slash and a name, no spaces, no second slash
|
|
271
|
-
// (Commands::Registry#unknown_command_word?). `/modle` is one; `/foo bar`
|
|
272
|
-
// and `/usr/bin/env` are not (they are prompts).
|
|
273
|
-
const UNKNOWN_COMMAND_WORD = /^\/[A-Za-z][A-Za-z0-9_-]*$/;
|
|
274
|
-
|
|
275
|
-
// The hint for a command word no command answers (a typo like `/modle`),
|
|
276
|
-
// or null when the line is not one: `Unknown command /modle. Did you mean
|
|
277
|
-
// /model? /help lists the commands.` The "Did you mean" part needs a close
|
|
278
|
-
// name among the session's commands and the page's own (webLocalReply).
|
|
279
|
-
export function unknownCommandHint(text, commands) {
|
|
280
|
-
const line = String(text || "").trim();
|
|
281
|
-
if (!UNKNOWN_COMMAND_WORD.test(line)) return null;
|
|
282
|
-
if (sessionCommandLine(line, commands)) return null;
|
|
283
|
-
const names = (Array.isArray(commands) ? commands : [])
|
|
284
|
-
.map((c) => (c && typeof c.name === "string" ? c.name : null))
|
|
285
|
-
.filter(Boolean)
|
|
286
|
-
.concat(Object.keys(WEB_LOCAL_REPLIES));
|
|
287
|
-
const close = closestName(line, names);
|
|
288
|
-
return `Unknown command ${line}. ${close ? `Did you mean ${close}? ` : ""}/help lists the commands.`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// The closest name within an optimal string alignment distance of 2, or
|
|
292
|
-
// null. Ties go to the first in the list.
|
|
293
|
-
function closestName(line, names) {
|
|
294
|
-
let best = null;
|
|
295
|
-
let bestDistance = 3;
|
|
296
|
-
for (const name of names) {
|
|
297
|
-
const distance = osaDistance(line, name);
|
|
298
|
-
if (distance < bestDistance) {
|
|
299
|
-
best = name;
|
|
300
|
-
bestDistance = distance;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return best;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Optimal string alignment distance: Levenshtein, with an adjacent
|
|
307
|
-
// transposition counting as 1 (so `/modle` is 1 from `/model`, as Ruby's
|
|
308
|
-
// DidYouMean reads it).
|
|
309
|
-
function osaDistance(a, b) {
|
|
310
|
-
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
311
|
-
for (let j = 0; j <= b.length; j += 1) rows[0][j] = j;
|
|
312
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
313
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
314
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
315
|
-
rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
|
|
316
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
317
|
-
rows[i][j] = Math.min(rows[i][j], rows[i - 2][j - 2] + 1);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return rows[a.length][b.length];
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// The terminal's own commands (a button in the web, or none yet): the
|
|
325
|
-
// reply the page shows itself (they never go to the worker, which doesn't
|
|
326
|
-
// know them), or null.
|
|
327
|
-
const WEB_LOCAL_REPLIES = {
|
|
328
|
-
"/archive": "/archive: use the archive button in the session bar",
|
|
329
|
-
"/detach": "/detach: a terminal's command; close the tab to leave, the worker keeps running",
|
|
330
|
-
"/exit": "/exit: close the tab to leave; stop in the session bar stops the worker",
|
|
331
|
-
"/quit": "/quit: close the tab to leave; stop in the session bar stops the worker",
|
|
332
|
-
"/recap": "/recap: not in the web yet; a recap shows here by itself when you come back to an idle session, and /recap in a terminal (chi --attach) makes one now",
|
|
333
|
-
"/stats": "/stats: not in the web yet; each turn shows its time, the session bar the context use and speed (hover ctx for tokens and cost), and /stats in a terminal (chi --attach) has the rest",
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
export function webLocalReply(text) {
|
|
337
|
-
const name = String(text || "").trim().split(/\s+/)[0].toLowerCase();
|
|
338
|
-
return WEB_LOCAL_REPLIES[name] || null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// The start page's first message when the page answers it itself
|
|
342
|
-
// (webLocalReply: /stats, /exit …): shown there, with no session made just
|
|
343
|
-
// to hold the reply. With images it is a message, as in a session.
|
|
344
|
-
export function startPageReply(text, { images = 0 } = {}) {
|
|
345
|
-
return images ? null : webLocalReply(text);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// The start page's reply to a first message that needs no session at all
|
|
349
|
-
// (2.29): its own command's reply (startPageReply), or the hint for a
|
|
350
|
-
// command word no command answers (a typo like /modle) — the hint the page
|
|
351
|
-
// shows in a session too, as a command bubble. Decided before the session
|
|
352
|
-
// is created, so a first /stats or /modle leaves none behind. A null here
|
|
353
|
-
// means the message goes to a session (a prompt, or anything with images).
|
|
354
|
-
export function startPageReplyOrHint(text, { commands = [], images = 0 } = {}) {
|
|
355
|
-
if (images) return null;
|
|
356
|
-
return startPageReply(text) || unknownCommandHint(text, commands);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// The continue card's answers, as the terminal's continue prompt reads them.
|
|
360
|
-
export function continueLine(choice, reason = "") {
|
|
361
|
-
const why = String(reason || "").trim();
|
|
362
|
-
if (choice === "no" && why) return `/continue no, ${why}`;
|
|
363
|
-
return `/continue ${choice}`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// How to show a command_ran: `label` names another client (null for this
|
|
367
|
-
// tab's own), `resync` says the conversation changed (!rollback, !cmd, a
|
|
368
|
-
// continue answered no) and must be read again. An anytime command's
|
|
369
|
-
// (`anytime`, D8) bubble is drawn at its command_queued, before the cards
|
|
370
|
-
// it shows; its command_ran fills that bubble in (`commandId`). So is a
|
|
371
|
-
// command queued for after the running turn (`queued`): its command_queued
|
|
372
|
-
// (waits: "turn_end") draws it `waiting`, saying so, until it ran or was
|
|
373
|
-
// dropped (styled as failed).
|
|
374
|
-
export const QUEUED_TEXT = "queued: runs after this turn";
|
|
375
|
-
|
|
376
|
-
export function commandView(event, myId) {
|
|
377
|
-
const own = isOwn(event.client_id, myId);
|
|
378
|
-
const waiting = event.waits === "turn_end";
|
|
379
|
-
return {
|
|
380
|
-
label: own ? null : clientLabel(event.client_id),
|
|
381
|
-
line: event.line || "",
|
|
382
|
-
text: event.output || (waiting ? QUEUED_TEXT : ""),
|
|
383
|
-
busy: event.status === "busy",
|
|
384
|
-
failed: event.status === "error" || event.status === "dropped",
|
|
385
|
-
resync: (event.changed || []).includes("messages"),
|
|
386
|
-
modelName: event.model_name || null,
|
|
387
|
-
anytime: event.anytime === true,
|
|
388
|
-
queued: waiting || event.queued === true,
|
|
389
|
-
waiting,
|
|
390
|
-
commandId: event.command_id || null,
|
|
391
|
-
// A card's action (Nudge…): no bubble unless it says something back.
|
|
392
|
-
hidden: event.card === true && !event.output && event.status !== "error",
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Whether a command_ran that re-read the conversation (`resync`) needs its
|
|
397
|
-
// own bubble: the redrawn history ends with a command bubble for its line
|
|
398
|
-
// (a `!cmd` appends its "!(<command>)\n<output>" message, which renders as
|
|
399
|
-
// one), so a second bubble would show the output twice. +historyBubbles+:
|
|
400
|
-
// the `.bubble.command` elements in document order; +line+: the event's
|
|
401
|
-
// line. Only the last one counts: it is the redrawn history's end.
|
|
402
|
-
export function historyHasCommandBubble(historyBubbles, line) {
|
|
403
|
-
const last = historyBubbles[historyBubbles.length - 1];
|
|
404
|
-
return !!last && last.querySelector(":scope > .command-line")?.textContent === line;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// What a prompt_restored (the worker rolled a failed turn back and handed its
|
|
408
|
-
// prompt back) does in this tab: `refill` is the text for an empty composer,
|
|
409
|
-
// only for a prompt this page sent (`sentIds`, its acks' enqueued_ids), so a
|
|
410
|
-
// replay after a reload never refills stale text; `own`/`label` mark the
|
|
411
|
-
// failed bubble shown again after the resync.
|
|
412
|
-
export function restoreAction(event, { myId, sentIds, composerEmpty }) {
|
|
413
|
-
const origin = event?.origin || {};
|
|
414
|
-
const own = isOwn(origin.client_id, myId);
|
|
415
|
-
const sent = own && !!origin.enqueued_id && sentIds.has(origin.enqueued_id);
|
|
416
|
-
// The images go back as chips with the text.
|
|
417
|
-
return withImages({
|
|
418
|
-
refill: sent && composerEmpty ? event.prompt : null,
|
|
419
|
-
own,
|
|
420
|
-
label: own ? null : clientLabel(origin.client_id),
|
|
421
|
-
}, sent && composerEmpty ? event.images : null);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// A turn that fails fast can have its prompt_restored come over the stream
|
|
425
|
-
// before the /turn ack names its enqueued_id: `early` (a Map) keeps such a
|
|
426
|
-
// restore of this tab's own prompt by that id, and the ack takes it back
|
|
427
|
-
// (restoreOnAck), so the prompt goes into the composer the ack would clear.
|
|
428
|
-
export function keepEarlyRestore(event, early, { myId, sentIds }) {
|
|
429
|
-
const origin = event?.origin || {};
|
|
430
|
-
const id = origin.enqueued_id;
|
|
431
|
-
if (!isOwn(origin.client_id, myId) || !id || sentIds.has(id)) return false;
|
|
432
|
-
early.set(id, event);
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// The ack's enqueued_id (already in sentIds) was restored early: what the
|
|
437
|
-
// composer gets back (as restoreAction for an empty composer), else null.
|
|
438
|
-
export function restoreOnAck(enqueuedId, early, { myId, sentIds }) {
|
|
439
|
-
const event = enqueuedId && early.get(enqueuedId);
|
|
440
|
-
if (!event) return null;
|
|
441
|
-
early.delete(enqueuedId);
|
|
442
|
-
return restoreAction(event, { myId, sentIds, composerEmpty: true });
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// The /turn this kept entry was for failed: no ack comes, so nothing would
|
|
446
|
-
// take it back. The whole map goes: this tab sends one turn at a time, so
|
|
447
|
-
// every entry waits for an ack that is now never coming (an older one is
|
|
448
|
-
// left over from a send that failed the same way).
|
|
449
|
-
export function dropEarlyRestores(early) {
|
|
450
|
-
const count = early.size;
|
|
451
|
-
early.clear();
|
|
452
|
-
return count;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// The composer's text with a restored prompt put back into it: the restored
|
|
456
|
-
// text alone when nothing is typed, else the typed text with the restored
|
|
457
|
-
// prompt under it (the user typed between the restore and the ack; their
|
|
458
|
-
// text must not be replaced).
|
|
459
|
-
export function restoreInto(refill, current) {
|
|
460
|
-
const restored = String(refill ?? "");
|
|
461
|
-
const typed = String(current ?? "");
|
|
462
|
-
if (!restored) return typed;
|
|
463
|
-
if (!typed.trim()) return restored;
|
|
464
|
-
return `${typed.replace(/\n+$/, "")}\n${restored}`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// What the composer holds once a /turn ack lands (4.13): the composer still
|
|
468
|
-
// carries the text that was just sent, so that text goes (it is the history's
|
|
469
|
-
// now) — but text typed since the send stays, and a prompt restored before
|
|
470
|
-
// this ack (restoreOnAck's refill) is put back without replacing it. The
|
|
471
|
-
// sent text is trimmed (turnText), so the composer's is too before they are
|
|
472
|
-
// compared: a trailing newline or space must not keep the message; and an
|
|
473
|
-
// image-only send (placeholders sent) leaves nothing worth keeping.
|
|
474
|
-
// @param sent the text the turn was sent with
|
|
475
|
-
// @param current the composer's text as the ack lands
|
|
476
|
-
// @param refill the prompt an early restore handed back, if any
|
|
477
|
-
export function composerAfterAck({ sent = "", current = "", refill = "" } = {}) {
|
|
478
|
-
const typed = String(current ?? "").trim();
|
|
479
|
-
const kept = !typed || typed === String(sent ?? "").trim() ? "" : current;
|
|
480
|
-
return restoreInto(refill, kept);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
239
|
// What an event does to the prompt bubbles, as a list of ops for app.js:
|
|
484
240
|
// add — a bubble for a prompt this tab hasn't shown (state queued /
|
|
485
241
|
// started / steered, labelled by its sender)
|
|
@@ -580,10 +336,11 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
580
336
|
closeText();
|
|
581
337
|
const call = { iteration: part.iteration, call_index: part.call_index, tool: part.tool };
|
|
582
338
|
if (part.label) call.label = part.label;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
339
|
+
// Its row fields, each in its place (activity.js ACTIVITY_FIELDS,
|
|
340
|
+
// EVENT_FIELDS), as the live events carry them.
|
|
341
|
+
const title = pickFields(part, ACTIVITY_FIELDS);
|
|
342
|
+
const fields = pickFields(part, EVENT_FIELDS);
|
|
343
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...fields });
|
|
587
344
|
if (part.status !== "running") {
|
|
588
345
|
// The activity as the live event had it; action and duration_ms
|
|
589
346
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -591,11 +348,13 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
591
348
|
const action = part.action ? { action: part.action } : {};
|
|
592
349
|
const completed = {
|
|
593
350
|
type: "tool_call_completed", ...call, output: part.output, output_truncated: !!part.output_truncated,
|
|
594
|
-
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title
|
|
351
|
+
activity: { ...action, tool: part.tool, status: part.status, params: part.params, ...title },
|
|
595
352
|
};
|
|
353
|
+
// A grep that found nothing: its row stays muted "no match".
|
|
354
|
+
if (part.no_match) completed.activity.no_match = true;
|
|
596
355
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
597
356
|
if (part.diff) completed.diff = part.diff;
|
|
598
|
-
|
|
357
|
+
Object.assign(completed, fields);
|
|
599
358
|
events.push(withImages(completed, part.images));
|
|
600
359
|
}
|
|
601
360
|
break;
|
|
@@ -637,6 +396,6 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
|
|
|
637
396
|
|
|
638
397
|
// +object+ with `images` when there are any (the shapes stay as they were
|
|
639
398
|
// for a turn without).
|
|
640
|
-
function withImages(object, images) {
|
|
399
|
+
export function withImages(object, images) {
|
|
641
400
|
return images?.length ? { ...object, images } : object;
|
|
642
401
|
}
|
|
@@ -7,6 +7,7 @@ import { commandBlockHtml, rowHover } from "./command_view.js";
|
|
|
7
7
|
import { copySourceAttr } from "./copy.js";
|
|
8
8
|
import { activityDiffHtml } from "./diff_view.js";
|
|
9
9
|
import { escapeHtml, userBodyHtml } from "./format.js";
|
|
10
|
+
import { refAttrs } from "./refs.js";
|
|
10
11
|
import { formatDuration } from "./timing.js";
|
|
11
12
|
import { emptyAnswerLine } from "./turn_events.js";
|
|
12
13
|
|
|
@@ -81,7 +82,10 @@ export function toolRowHtml(row, { thumbs = () => "" } = {}) {
|
|
|
81
82
|
export function toolRowInnerHtml(row, { thumbs = () => "" } = {}) {
|
|
82
83
|
const shown = row.title || row.params;
|
|
83
84
|
const hover = rowHover(row) ? ` title="${escapeHtml(rowHover(row))}"` : "";
|
|
84
|
-
|
|
85
|
+
// A row with a ref (a file tool's file) carries it on its title, which
|
|
86
|
+
// opens it, and its diff lines open it at their line (refs.js).
|
|
87
|
+
const ref = shown ? refAttrs(row.ref) : "";
|
|
88
|
+
const params = shown ? `<span class="activity-params${ref ? " ref" : ""}"${hover}${ref}>${escapeHtml(shown)}</span>` : "";
|
|
85
89
|
// The name the model called the tool by when it was an alias's, dim.
|
|
86
90
|
const alias = row.called_as ? `<span class="activity-alias">(called as ${escapeHtml(row.called_as)})</span>` : "";
|
|
87
91
|
const duration = formatDuration(row.duration_ms);
|
|
@@ -66,7 +66,7 @@ module Samagotchi
|
|
|
66
66
|
# @return [Integer] the exit status
|
|
67
67
|
def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false,
|
|
68
68
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
|
|
69
|
-
new_token: false)
|
|
69
|
+
editor: Config::BY_KEY["web.editor"].default, new_token: false)
|
|
70
70
|
rotate_token if new_token
|
|
71
71
|
setting = host_setting(host)
|
|
72
72
|
lan = Lan.wanted?(setting) ? Lan.choose(setting) : nil
|
|
@@ -95,7 +95,7 @@ module Samagotchi
|
|
|
95
95
|
return 0
|
|
96
96
|
end
|
|
97
97
|
if start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, view: view,
|
|
98
|
-
annotate_presets: annotate_presets, lan: lan)
|
|
98
|
+
annotate_presets: annotate_presets, editor: editor, lan: lan)
|
|
99
99
|
0
|
|
100
100
|
else
|
|
101
101
|
1
|
|
@@ -119,7 +119,7 @@ module Samagotchi
|
|
|
119
119
|
# @return [Boolean] false when the port was taken (said so on stderr)
|
|
120
120
|
def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
|
|
121
121
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
|
|
122
|
-
token_path: Token.path)
|
|
122
|
+
editor: Config::BY_KEY["web.editor"].default, token_path: Token.path)
|
|
123
123
|
port = resolve_port(port)
|
|
124
124
|
host = resolve_host(host)
|
|
125
125
|
url ||= "http://#{url_host(host)}:#{port}/"
|
|
@@ -130,7 +130,7 @@ module Samagotchi
|
|
|
130
130
|
lan_option = { ip: lan.ip, token: Token::Source.new(token_path) }
|
|
131
131
|
end
|
|
132
132
|
app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, view: view,
|
|
133
|
-
annotate_presets: annotate_presets, hub: hub, lan: lan_option)
|
|
133
|
+
annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option)
|
|
134
134
|
Samagotchi::Log.info(:web, "start", url: "http://#{host}:#{port}", version: Samagotchi::VERSION)
|
|
135
135
|
Samagotchi::Log.info(:web, "lan", ip: lan.ip, interface: lan.interface) if lan
|
|
136
136
|
# Subscribed before the first scan, which makes the first check.
|
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.55.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -260,6 +260,7 @@ files:
|
|
|
260
260
|
- lib/samagotchi/engine_peers.rb
|
|
261
261
|
- lib/samagotchi/events.rb
|
|
262
262
|
- lib/samagotchi/extension_load.rb
|
|
263
|
+
- lib/samagotchi/file_ref.rb
|
|
263
264
|
- lib/samagotchi/gem_update.rb
|
|
264
265
|
- lib/samagotchi/generation_phase.rb
|
|
265
266
|
- lib/samagotchi/git_head.rb
|
|
@@ -457,6 +458,7 @@ files:
|
|
|
457
458
|
- lib/samagotchi/tool_declarations.rb
|
|
458
459
|
- lib/samagotchi/tool_ids.rb
|
|
459
460
|
- lib/samagotchi/tool_response.rb
|
|
461
|
+
- lib/samagotchi/tool_row_fields.rb
|
|
460
462
|
- lib/samagotchi/tool_runner.rb
|
|
461
463
|
- lib/samagotchi/tool_view.rb
|
|
462
464
|
- lib/samagotchi/tools/args.rb
|
|
@@ -506,6 +508,7 @@ files:
|
|
|
506
508
|
- lib/samagotchi/waiting_steer.rb
|
|
507
509
|
- lib/samagotchi/web/app.rb
|
|
508
510
|
- lib/samagotchi/web/capabilities.rb
|
|
511
|
+
- lib/samagotchi/web/editor.rb
|
|
509
512
|
- lib/samagotchi/web/info_probe.rb
|
|
510
513
|
- lib/samagotchi/web/lan.rb
|
|
511
514
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
@@ -521,6 +524,8 @@ files:
|
|
|
521
524
|
- lib/samagotchi/web/public/command_complete.js
|
|
522
525
|
- lib/samagotchi/web/public/command_list.js
|
|
523
526
|
- lib/samagotchi/web/public/command_view.js
|
|
527
|
+
- lib/samagotchi/web/public/commands.js
|
|
528
|
+
- lib/samagotchi/web/public/composer.js
|
|
524
529
|
- lib/samagotchi/web/public/composer_size.js
|
|
525
530
|
- lib/samagotchi/web/public/context_chips.js
|
|
526
531
|
- lib/samagotchi/web/public/copy.js
|
|
@@ -532,8 +537,10 @@ files:
|
|
|
532
537
|
- lib/samagotchi/web/public/hold.js
|
|
533
538
|
- lib/samagotchi/web/public/images.js
|
|
534
539
|
- lib/samagotchi/web/public/index.html
|
|
540
|
+
- lib/samagotchi/web/public/info_bar.js
|
|
535
541
|
- lib/samagotchi/web/public/init_row.js
|
|
536
542
|
- lib/samagotchi/web/public/kept_commands.js
|
|
543
|
+
- lib/samagotchi/web/public/lists_view.js
|
|
537
544
|
- lib/samagotchi/web/public/live_timing.js
|
|
538
545
|
- lib/samagotchi/web/public/live_turn.js
|
|
539
546
|
- lib/samagotchi/web/public/llm_context_chip.js
|
|
@@ -544,6 +551,8 @@ files:
|
|
|
544
551
|
- lib/samagotchi/web/public/prompt_history.js
|
|
545
552
|
- lib/samagotchi/web/public/question_card.js
|
|
546
553
|
- lib/samagotchi/web/public/question_cards.js
|
|
554
|
+
- lib/samagotchi/web/public/refs.js
|
|
555
|
+
- lib/samagotchi/web/public/restore.js
|
|
547
556
|
- lib/samagotchi/web/public/route.js
|
|
548
557
|
- lib/samagotchi/web/public/scope.js
|
|
549
558
|
- lib/samagotchi/web/public/scroll.js
|
|
@@ -552,9 +561,9 @@ files:
|
|
|
552
561
|
- lib/samagotchi/web/public/session_title.js
|
|
553
562
|
- lib/samagotchi/web/public/sessions_list.js
|
|
554
563
|
- lib/samagotchi/web/public/shown_texts.js
|
|
555
|
-
- lib/samagotchi/web/public/stable_html.js
|
|
556
564
|
- lib/samagotchi/web/public/stage_model.js
|
|
557
565
|
- lib/samagotchi/web/public/stage_view.js
|
|
566
|
+
- lib/samagotchi/web/public/stream_handlers.js
|
|
558
567
|
- lib/samagotchi/web/public/strip.js
|
|
559
568
|
- lib/samagotchi/web/public/tally.js
|
|
560
569
|
- lib/samagotchi/web/public/task_stop.js
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// A block redrawn every tick (the info bar, once a second while a turn
|
|
2
|
-
// runs) keeps its nodes: +html+ is written only when it changed, so a
|
|
3
|
-
// shown tooltip stays up and a press on one of its buttons isn't lost to a
|
|
4
|
-
// rewrite between mousedown and mouseup. +live+ ({selector: text}) names
|
|
5
|
-
// the parts that change every tick (a running clock): +html+ carries them
|
|
6
|
-
// empty and only their text is set, so a tick alone rewrites nothing else.
|
|
7
|
-
const shown = new WeakMap();
|
|
8
|
-
|
|
9
|
-
export function renderStable(el, html, live = {}) {
|
|
10
|
-
if (shown.get(el) !== html) {
|
|
11
|
-
el.innerHTML = html;
|
|
12
|
-
shown.set(el, html);
|
|
13
|
-
}
|
|
14
|
-
for (const [selector, text] of Object.entries(live)) {
|
|
15
|
-
const part = el.querySelector(selector);
|
|
16
|
-
if (part && part.textContent !== text) part.textContent = text;
|
|
17
|
-
}
|
|
18
|
-
}
|