samagotchi 0.13.0 → 0.15.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 +82 -1
- data/bin/chi +4 -7
- data/docs/configuration.md +50 -7
- data/docs/guardrails.md +2 -1
- data/docs/plugins.md +28 -13
- data/docs/sessions.md +1 -1
- data/docs/sub-agent.md +1 -1
- data/lib/samagotchi/bridge/card_store.rb +94 -6
- data/lib/samagotchi/bridge.rb +3 -1
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +68 -5
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +166 -34
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +31 -6
- data/lib/samagotchi/context_window.rb +30 -5
- data/lib/samagotchi/engine.rb +21 -7
- data/lib/samagotchi/guardrails/parent_approvals.rb +5 -2
- data/lib/samagotchi/host_registry.rb +31 -6
- data/lib/samagotchi/kernel_loop.rb +1 -1
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/turn_settings.rb +7 -5
- data/lib/samagotchi/plugin_session_state.rb +30 -0
- data/lib/samagotchi/self_report.rb +24 -4
- data/lib/samagotchi/session_manager.rb +8 -0
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/session_retention.rb +6 -1
- data/lib/samagotchi/system_prompt.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +65 -13
- data/lib/samagotchi/terminal_ui.rb +27 -3
- data/lib/samagotchi/tools/delegate_wait.rb +17 -10
- data/lib/samagotchi/tools/memory.rb +3 -3
- data/lib/samagotchi/utf8_default.rb +29 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -7
- data/lib/samagotchi/web/public/activity.js +6 -1
- data/lib/samagotchi/web/public/app.js +46 -10
- data/lib/samagotchi/web/public/index.html +10 -1
- data/lib/samagotchi/web/public/notify.js +16 -3
- data/lib/samagotchi/web/public/question_cards.js +3 -1
- data/lib/samagotchi/web/public/sessions_list.js +45 -0
- data/lib/samagotchi/web/public/strip.js +8 -1
- data/lib/samagotchi/web/public/timing.js +11 -2
- data/lib/samagotchi/web/public/turn_view.js +10 -10
- metadata +3 -1
|
@@ -12,6 +12,9 @@ export const ROW_STATUS = Object.freeze({
|
|
|
12
12
|
ERROR: "error",
|
|
13
13
|
// A task_wait the user's Stop ended (the task itself runs on).
|
|
14
14
|
STOPPED: "stopped",
|
|
15
|
+
// A call a guardrail denied (a hook, the guardrails bundle, a parent's
|
|
16
|
+
// or the user's Deny): it never ran, and it isn't a failed call either.
|
|
17
|
+
BLOCKED: "blocked",
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
// What a row shows for its tool: a plugin tool's label ("chrome:
|
|
@@ -101,9 +104,11 @@ export function addCompleted(model, event = {}) {
|
|
|
101
104
|
return row;
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
// A completed call's row status from the server's: error, stopped,
|
|
107
|
+
// A completed call's row status from the server's: error, stopped,
|
|
108
|
+
// blocked, else ok.
|
|
105
109
|
export function completedStatus(status) {
|
|
106
110
|
if (status === "error") return ROW_STATUS.ERROR;
|
|
107
111
|
if (status === "stopped") return ROW_STATUS.STOPPED;
|
|
112
|
+
if (status === "blocked") return ROW_STATUS.BLOCKED;
|
|
108
113
|
return ROW_STATUS.OK;
|
|
109
114
|
}
|
|
@@ -38,7 +38,7 @@ import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
|
38
38
|
import { allScopeHref, cardFolder, projectScopeHref, scopeDir } from "./scope.js";
|
|
39
39
|
import { shouldFollowScroll, keepFollowing } from "./scroll.js";
|
|
40
40
|
import { stripColumns, stripShowParts } from "./strip.js";
|
|
41
|
-
import { applySessionEvent, listedSessions, sortedByUpdated, withChildrenAfterParents } from "./sessions_list.js";
|
|
41
|
+
import { applySessionEvent, heldOrder, listedSessions, sortedByUpdated, waitingBadge, waitingFirst, waitingSearchText, withChildrenAfterParents } from "./sessions_list.js";
|
|
42
42
|
import { createNotifications } from "./notifications.js";
|
|
43
43
|
import {
|
|
44
44
|
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, normalizeTiming, turnTimingText,
|
|
@@ -262,12 +262,15 @@ function filteredSessions(list) {
|
|
|
262
262
|
(s.first_preview || "").toLowerCase().includes(q) ||
|
|
263
263
|
s.short_id.toLowerCase().includes(q) ||
|
|
264
264
|
s.id.toLowerCase().includes(q) ||
|
|
265
|
-
s.status.toLowerCase().includes(q)
|
|
265
|
+
s.status.toLowerCase().includes(q) ||
|
|
266
|
+
waitingSearchText(s).includes(q),
|
|
266
267
|
);
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
// A card: the first message as its title, then status, how long ago, owner
|
|
270
|
-
// and memories. The id is in the tooltip (and the search) only.
|
|
271
|
+
// and memories. The id is in the tooltip (and the search) only. A session
|
|
272
|
+
// that waits on the user (a question, an approval, a card with actions)
|
|
273
|
+
// wears the warning colour and says what for.
|
|
271
274
|
// deletable: the all-sessions view's cards get a delete button.
|
|
272
275
|
function cardHtml(s, { deletable = false } = {}) {
|
|
273
276
|
const ts = s.updated_at || s.created_at || "";
|
|
@@ -281,7 +284,10 @@ function cardHtml(s, { deletable = false } = {}) {
|
|
|
281
284
|
const archivedHtml = s.archived ? `<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>` : "";
|
|
282
285
|
const ctx = cardCtxText(s.ctx_pct);
|
|
283
286
|
const ctxHtml = ctx ? `<span class="ctx" title="Context used after the last turn">${escapeHtml(ctx)}</span>` : "";
|
|
284
|
-
|
|
287
|
+
const waiting = waitingBadge(s);
|
|
288
|
+
const waitingHtml = waiting ? `<span class="attn ${waiting.kind}" title="${escapeHtml(waiting.title)}">${escapeHtml(waiting.text)}</span>` : "";
|
|
289
|
+
const dotWord = waiting ? waiting.title : s.status;
|
|
290
|
+
return `<div class="card${s.archived ? " archived" : ""}${waiting ? " waiting" : ""}" data-id="${s.id}" title="${escapeHtml(tip)}${memTitle ? ` · ${escapeHtml(memTitle)}` : ""}"><div class="preview">${preview}</div>${s.recap ? `<div class="recap" title="${escapeHtml(s.recap)}">${escapeHtml(s.recap)}</div>` : ""}<div class="meta"><span class="status dot ${escapeHtml(s.status)}${waiting ? " waiting" : ""}" title="${escapeHtml(dotWord)}" aria-label="${escapeHtml(dotWord)}"></span>${waitingHtml}${timeHtml}${ctxHtml}${folderHtml}${parentChipHtml(s)}${ownerBadgeHtml(s)}${archivedHtml}${s.used_memory_names && s.used_memory_names.length ? `<span title="${escapeHtml(s.used_memory_names.join(", "))}">mem ${s.used_memory_names.length}</span>` : ""}</div>${deletable ? `<button type="button" class="card-del" data-del="${escapeHtml(s.id)}" title="Delete session" aria-label="Delete session ${escapeHtml(s.short_id)}">\u2715</button>` : ""}</div>`;
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
// A delegated session's `↳ <parent>` chip; the parent's preview is the tooltip.
|
|
@@ -304,9 +310,30 @@ function fitStripColumns() {
|
|
|
304
310
|
topStripEl.style.setProperty("--strip-cols", stripCols);
|
|
305
311
|
}
|
|
306
312
|
|
|
313
|
+
// The order a list draws its cards in: the sessions that wait on the user
|
|
314
|
+
// first (waitingFirst), back to their place once answered. Never under the
|
|
315
|
+
// pointer, though: while it is over the list the order last drawn holds,
|
|
316
|
+
// and its leaving draws the list again.
|
|
317
|
+
const drawnOrder = new Map(); // list element → ids in the order last drawn
|
|
318
|
+
const heldLists = new Set(); // lists drawn in a held order
|
|
319
|
+
const canHover = globalThis.matchMedia?.("(hover: hover)").matches ?? false;
|
|
320
|
+
function listOrder(el, list) {
|
|
321
|
+
const next = waitingFirst(list);
|
|
322
|
+
const held = canHover && el.matches(":hover") ? drawnOrder.get(el) : null;
|
|
323
|
+
const ordered = held ? heldOrder(held, next) : next;
|
|
324
|
+
if (held && ordered.some((s, i) => s !== next[i])) heldLists.add(el);
|
|
325
|
+
else heldLists.delete(el);
|
|
326
|
+
drawnOrder.set(el, ordered.map((s) => s.id));
|
|
327
|
+
return ordered;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function stripSessions() {
|
|
331
|
+
return listOrder(topStripEl, shownSessions()).slice(0, stripCols);
|
|
332
|
+
}
|
|
333
|
+
|
|
307
334
|
function render() {
|
|
308
335
|
fitStripColumns();
|
|
309
|
-
renderTop(
|
|
336
|
+
renderTop(stripSessions());
|
|
310
337
|
if (allViewOpen()) renderAll();
|
|
311
338
|
}
|
|
312
339
|
|
|
@@ -316,7 +343,7 @@ window.addEventListener("resize", () => {
|
|
|
316
343
|
stripResizeTimer = setTimeout(() => {
|
|
317
344
|
const before = stripCols;
|
|
318
345
|
fitStripColumns();
|
|
319
|
-
if (stripCols !== before && allSessions.length) renderTop(
|
|
346
|
+
if (stripCols !== before && allSessions.length) renderTop(stripSessions());
|
|
320
347
|
}, 150);
|
|
321
348
|
});
|
|
322
349
|
|
|
@@ -359,18 +386,20 @@ function renderTop(sessions) {
|
|
|
359
386
|
$("#stripHide").addEventListener("click", () => setStripHidden(true));
|
|
360
387
|
}
|
|
361
388
|
|
|
362
|
-
// The hidden strip's pill: "▾ 5 sessions · ● 1 live
|
|
389
|
+
// The hidden strip's pill: "▾ 5 sessions · ● 1 live · ● 1 waiting",
|
|
390
|
+
// following the list.
|
|
363
391
|
function renderStripShow() {
|
|
364
|
-
const { count, live } = stripShowParts(shownSessions());
|
|
392
|
+
const { count, live, waiting } = stripShowParts(shownSessions());
|
|
365
393
|
const liveHtml = live ? ` · <span class="live"><span class="live-dot" aria-hidden="true"></span>${escapeHtml(live)}</span>` : "";
|
|
366
|
-
|
|
394
|
+
const waitingHtml = waiting ? ` · <span class="waiting"><span class="waiting-dot" aria-hidden="true"></span>${escapeHtml(waiting)}</span>` : "";
|
|
395
|
+
$("#stripShow").innerHTML = `<span class="pill-arrow" aria-hidden="true">\u25BE</span> ${escapeHtml(count)}${liveHtml}${waitingHtml}`;
|
|
367
396
|
}
|
|
368
397
|
|
|
369
398
|
// A delegated session sits right after its parent: applied on every
|
|
370
399
|
// render, since a hub snapshot hands the list back in the server's order.
|
|
371
400
|
function renderAll() {
|
|
372
401
|
const listed = allViewSessions();
|
|
373
|
-
const sessions = withChildrenAfterParents(filteredSessions(listed));
|
|
402
|
+
const sessions = listOrder(allListEl, withChildrenAfterParents(filteredSessions(listed)));
|
|
374
403
|
$("#count").textContent = sessions.length === listed.length
|
|
375
404
|
? `${listed.length}`
|
|
376
405
|
: `${sessions.length} / ${listed.length}`;
|
|
@@ -2208,6 +2237,13 @@ setStripHidden(stripHiddenSaved());
|
|
|
2208
2237
|
$("#stripShow").addEventListener("click", () => setStripHidden(false));
|
|
2209
2238
|
|
|
2210
2239
|
$("#filter").addEventListener("input", renderAll);
|
|
2240
|
+
// A list whose order was held under the pointer (listOrder) catches up.
|
|
2241
|
+
topStripEl.addEventListener("pointerleave", () => {
|
|
2242
|
+
if (heldLists.has(topStripEl)) renderTop(stripSessions());
|
|
2243
|
+
});
|
|
2244
|
+
allListEl.addEventListener("pointerleave", () => {
|
|
2245
|
+
if (heldLists.has(allListEl) && allViewOpen()) renderAll();
|
|
2246
|
+
});
|
|
2211
2247
|
// Esc leaves the all-sessions view.
|
|
2212
2248
|
document.addEventListener("keydown", (e) => {
|
|
2213
2249
|
if (e.key === "Escape" && allViewOpen()) closeAllView();
|
|
@@ -14,6 +14,9 @@ body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetic
|
|
|
14
14
|
#dock,.turn-stage,.dock-card,#topStrip .card,#allList .card,.bubble.question,.bubble.plugin-card{background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
|
|
15
15
|
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:rgba(255,255,255,.14)}
|
|
16
16
|
#topStrip .card.active,#allList .card.active{background:color-mix(in srgb,var(--accent-bg) 75%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
|
|
17
|
+
/* A session that waits on the user (a question, an approval, a card with actions): the warning colour of its question card, also when active or hovered. */
|
|
18
|
+
#topStrip .card.waiting,#allList .card.waiting{border-color:color-mix(in srgb,var(--warn) 75%,transparent)}
|
|
19
|
+
#topStrip .card.waiting:hover,#allList .card.waiting:hover{border-color:var(--warn)}
|
|
17
20
|
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){#dock,.turn-stage,.dock-card,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel)}.bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel))}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel))}}
|
|
18
21
|
@media (prefers-reduced-transparency:reduce){#dock,.turn-stage,.dock-card,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}}
|
|
19
22
|
body.no-glass #dock,body.no-glass .turn-stage,body.no-glass .dock-card,body.no-glass #topStrip .card,body.no-glass #allList .card,body.no-glass #allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
@@ -111,6 +114,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
111
114
|
.activity-status.ok{color:var(--ok)}
|
|
112
115
|
.activity-status.error{color:var(--danger)}
|
|
113
116
|
.activity-status.stopped{color:var(--warn-text);opacity:.8}
|
|
117
|
+
.activity-status.blocked{color:var(--danger-text)}
|
|
114
118
|
.activity-tool{color:var(--accent);font-weight:600}
|
|
115
119
|
.activity-params{color:var(--text-muted);word-break:break-all}
|
|
116
120
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
@@ -632,6 +636,8 @@ body.has-session #scope .scope-where{display:none}
|
|
|
632
636
|
#stripShow{display:none;white-space:nowrap;flex-shrink:0}
|
|
633
637
|
#stripShow .live{color:var(--ok-text);white-space:nowrap}
|
|
634
638
|
#stripShow .live-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:4px;vertical-align:1px}
|
|
639
|
+
#stripShow .waiting{color:var(--warn-text);white-space:nowrap}
|
|
640
|
+
#stripShow .waiting-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--warn);margin-right:4px;vertical-align:1px}
|
|
635
641
|
#stripHide{align-self:flex-end;border:0;padding:0 2px;border-radius:4px}
|
|
636
642
|
#stripHide:hover{text-decoration:underline;color:var(--text-2)}
|
|
637
643
|
body.strip-hidden #stripShow{display:block}
|
|
@@ -687,7 +693,10 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
687
693
|
.card .dot.completed{background:var(--accent)}
|
|
688
694
|
.card .dot.error{background:var(--danger)}
|
|
689
695
|
@keyframes dot-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 55%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--ok) 0%,transparent)}}
|
|
690
|
-
|
|
696
|
+
.card .dot.waiting{background:var(--warn);animation:attn-pulse 1.6s ease-in-out infinite}
|
|
697
|
+
@keyframes attn-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--warn) 60%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--warn) 0%,transparent)}}
|
|
698
|
+
@media (prefers-reduced-motion:reduce){.card .dot.running,.card .dot.waiting{animation:none}}
|
|
699
|
+
.card .attn{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn-bg);color:var(--warn-text);border:1px solid var(--warn-border)}
|
|
691
700
|
.status.running{background:var(--ok-bg);color:var(--ok-text)}
|
|
692
701
|
.status.idle{background:var(--bg-raised)}
|
|
693
702
|
.status.completed{background:var(--accent-bg);color:var(--accent)}
|
|
@@ -15,6 +15,21 @@ export function notifyState({ hasNotification, secureContext, permission, wanted
|
|
|
15
15
|
// A finished turn shorter than this was watched: no "done".
|
|
16
16
|
export const DONE_MIN_SECONDS = 10;
|
|
17
17
|
|
|
18
|
+
const questionReason = (q) => (q.kind === "approval" ? "approval" : "question");
|
|
19
|
+
|
|
20
|
+
// What a session waits on the user for right now, from one summary: an open
|
|
21
|
+
// question or approval, or a running turn's card with actions (check-in's
|
|
22
|
+
// Nudge / Keep going / Stop asks as a question does). The session cards
|
|
23
|
+
// mark and lift these; attentionFor notifies when one opens.
|
|
24
|
+
// @return null | {reason: "question"|"approval"|"card", id}
|
|
25
|
+
export function waitingOn(summary) {
|
|
26
|
+
const q = summary?.pending_question;
|
|
27
|
+
if (q && q.id) return { reason: questionReason(q), id: q.id };
|
|
28
|
+
const card = summary?.pending_card;
|
|
29
|
+
if (card && card.id) return { reason: "card", id: card.id };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
// What changed between two summaries of one session that needs the user.
|
|
19
34
|
// @param prev the summary held before (null for a session not seen yet)
|
|
20
35
|
// @param next the new summary
|
|
@@ -24,10 +39,8 @@ export function attentionFor(prev, next) {
|
|
|
24
39
|
if (!next || !next.id) return null;
|
|
25
40
|
const q = next.pending_question;
|
|
26
41
|
if (q && q.id && q.id !== prev?.pending_question?.id) {
|
|
27
|
-
return { reason: q
|
|
42
|
+
return { reason: questionReason(q), sessionId: next.id, key: `${next.id}:${q.id}` };
|
|
28
43
|
}
|
|
29
|
-
// A running turn's card with actions (check-in's Nudge / Keep going /
|
|
30
|
-
// Stop) asks the user as a question does.
|
|
31
44
|
const card = next.pending_card;
|
|
32
45
|
if (card && card.id && card.id !== prev?.pending_card?.id) {
|
|
33
46
|
return { reason: "card", sessionId: next.id, key: cardKey(next.id, card.id) };
|
|
@@ -158,9 +158,11 @@ export function createQuestionCards({
|
|
|
158
158
|
});
|
|
159
159
|
card.appendChild(dismiss);
|
|
160
160
|
|
|
161
|
+
// Read before the card makes the history taller.
|
|
162
|
+
const follow = isNearBottom();
|
|
161
163
|
place(card);
|
|
162
164
|
questionEl = card;
|
|
163
|
-
if (
|
|
165
|
+
if (follow) scrollToEnd();
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
// The tool, the command (or paths, or args) as code, where and why; no
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The page's session list as a projection of the hub's events
|
|
2
2
|
// (GET /api/events): a pure reducer over the list the page holds.
|
|
3
|
+
import { waitingOn } from "./notify.js";
|
|
3
4
|
|
|
4
5
|
// @param list the current list (never mutated)
|
|
5
6
|
// @param type "snapshot" | "session" | "session_gone"
|
|
@@ -65,3 +66,47 @@ export function withChildrenAfterParents(list) {
|
|
|
65
66
|
const ordered = heads.slice().sort((a, b) => familyMs(b) - familyMs(a));
|
|
66
67
|
return ordered.flatMap((s) => [s, ...(children.get(s.id) || [])]);
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
const WAITING_BADGES = {
|
|
71
|
+
question: { text: "question", title: "Waiting for your answer" },
|
|
72
|
+
approval: { text: "approval", title: "Waiting for your approval" },
|
|
73
|
+
card: { text: "needs you", title: "A card in the running turn waits for you" },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// A card's "needs you" badge: what the session waits on the user for
|
|
77
|
+
// (notify.js waitingOn), or null.
|
|
78
|
+
// @return null | {kind, text, title}
|
|
79
|
+
export function waitingBadge(summary) {
|
|
80
|
+
const waiting = waitingOn(summary);
|
|
81
|
+
return waiting ? { kind: waiting.reason, ...WAITING_BADGES[waiting.reason] } : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// What the all view's search matches for a session that waits on the user:
|
|
85
|
+
// "waiting" and its badge's word ("question", "approval", "needs you");
|
|
86
|
+
// "" for one that doesn't.
|
|
87
|
+
export function waitingSearchText(summary) {
|
|
88
|
+
const badge = waitingBadge(summary);
|
|
89
|
+
return badge ? `waiting ${badge.text}` : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The sessions that wait on the user (waitingOn) first, the rest after,
|
|
93
|
+
// each part in the list's order: an answered one goes back to its place.
|
|
94
|
+
// A delegated session moves with its parent when both are listed (a
|
|
95
|
+
// family waits when any of it does), so withChildrenAfterParents' groups
|
|
96
|
+
// stay together.
|
|
97
|
+
export function waitingFirst(list) {
|
|
98
|
+
const ids = new Set(list.map((s) => s.id));
|
|
99
|
+
const family = (s) => (s.parent_id && ids.has(s.parent_id) ? s.parent_id : s.id);
|
|
100
|
+
const waiting = new Set(list.filter((s) => waitingOn(s)).map(family));
|
|
101
|
+
if (!waiting.size) return list;
|
|
102
|
+
return [...list.filter((s) => waiting.has(family(s))), ...list.filter((s) => !waiting.has(family(s)))];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The list in an order shown before (+ids+): what the pointer is over
|
|
106
|
+
// doesn't move. Sessions new since then go after, in the list's order;
|
|
107
|
+
// gone ones drop out.
|
|
108
|
+
export function heldOrder(ids, list) {
|
|
109
|
+
const at = new Map(ids.map((id, i) => [id, i]));
|
|
110
|
+
const known = list.filter((s) => at.has(s.id)).sort((a, b) => at.get(a.id) - at.get(b.id));
|
|
111
|
+
return [...known, ...list.filter((s) => !at.has(s.id))];
|
|
112
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { waitingOn } from "./notify.js";
|
|
2
|
+
|
|
1
3
|
// How many session cards the top strip shows for its width in px: a card
|
|
2
4
|
// needs about 360 px plus a 12 px gap next to the 150 px "All sessions" tile;
|
|
3
5
|
// never fewer than 3 (narrow screens scroll them), never more than 6.
|
|
@@ -13,13 +15,18 @@ export function stripColumns(width) {
|
|
|
13
15
|
// The hidden strip's "▾ sessions" pill carries the list's count, and how
|
|
14
16
|
// many of those sessions a worker runs (the cards' green "live" badge), so a
|
|
15
17
|
// user who hid the strip still sees there is a live session to go back to.
|
|
16
|
-
//
|
|
18
|
+
// And how many wait on the user (notify.js waitingOn: a question, an
|
|
19
|
+
// approval, a card), so a hidden strip still says one needs an answer.
|
|
20
|
+
// Plain text parts; live and waiting are "" when none. Zero sessions keep
|
|
21
|
+
// the bare label.
|
|
17
22
|
export function stripShowParts(sessions) {
|
|
18
23
|
const list = sessions || [];
|
|
19
24
|
const n = list.length;
|
|
20
25
|
const liveN = list.filter((s) => s && s.owner === "worker").length;
|
|
26
|
+
const waitingN = list.filter((s) => waitingOn(s)).length;
|
|
21
27
|
return {
|
|
22
28
|
count: n ? `${n} ${n === 1 ? "session" : "sessions"}` : "sessions",
|
|
23
29
|
live: liveN ? `${liveN} live` : "",
|
|
30
|
+
waiting: waitingN ? `${waitingN} waiting` : "",
|
|
24
31
|
};
|
|
25
32
|
}
|
|
@@ -105,7 +105,7 @@ export function turnGroups(items, timing) {
|
|
|
105
105
|
turn.merged.push(i);
|
|
106
106
|
} else if (item?.role === "user") {
|
|
107
107
|
turnIndex += 1;
|
|
108
|
-
turn = { kind: "turn", turnIndex, user: i, texts: [], steers: [], merged: [] };
|
|
108
|
+
turn = { kind: "turn", turnIndex, turnId: item.turn_id, user: i, texts: [], steers: [], merged: [] };
|
|
109
109
|
groups.push(turn);
|
|
110
110
|
} else if (item?.role === "assistant") {
|
|
111
111
|
if (turn) turn.texts.push(i);
|
|
@@ -138,8 +138,17 @@ function withSteers(group, steerIndexes, items) {
|
|
|
138
138
|
return { ...group, steps, steers: left };
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// A prompt's turn record: the one with its turn id (the engine saves it
|
|
142
|
+
// on the prompt), so a turn whose prompt left the conversation (a failed
|
|
143
|
+
// one's went back to the user, !rollback) shifts no other; a prompt saved
|
|
144
|
+
// before turn ids pairs by its place among the prompts.
|
|
145
|
+
function promptRecord(timing, g) {
|
|
146
|
+
if (typeof g.turnId !== "string" || !g.turnId) return turnRecordAt(timing, g.turnIndex);
|
|
147
|
+
return (timing?.turnRecords || []).find((r) => r.id === g.turnId) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
function turnGroup(g, items, timing) {
|
|
142
|
-
const record =
|
|
151
|
+
const record = promptRecord(timing, g);
|
|
143
152
|
const tools = record ? (timing?.toolRecords || []).filter((r) => r.turn_id === record.id) : [];
|
|
144
153
|
const stepsOnly = canceledSteps(g, record, tools);
|
|
145
154
|
if (g.texts.some((i) => items[i].parts)) return partsTurn(g, items, record, tools, stepsOnly);
|
|
@@ -24,19 +24,16 @@ import * as model from "./turn_model.js";
|
|
|
24
24
|
import { createHold } from "./hold.js";
|
|
25
25
|
|
|
26
26
|
// A row's status (live, or a saved tool record's on reload): running,
|
|
27
|
-
// error, stopped (a task_wait the user's Stop ended),
|
|
27
|
+
// error, stopped (a task_wait the user's Stop ended), blocked (a guardrail
|
|
28
|
+
// denied it), else ok ("done").
|
|
29
|
+
const NAMED_STATUSES = ["running", "error", "stopped", "blocked"];
|
|
30
|
+
|
|
28
31
|
function activityStatusClass(status) {
|
|
29
|
-
|
|
30
|
-
if (status === "error") return "error";
|
|
31
|
-
if (status === "stopped") return "stopped";
|
|
32
|
-
return "ok";
|
|
32
|
+
return NAMED_STATUSES.includes(status) ? status : "ok";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function activityStatusLabel(status) {
|
|
36
|
-
|
|
37
|
-
if (status === "error") return "error";
|
|
38
|
-
if (status === "stopped") return "stopped";
|
|
39
|
-
return "done";
|
|
36
|
+
return NAMED_STATUSES.includes(status) ? status : "done";
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
// A live tool row filled from its activity row (status, title, output).
|
|
@@ -690,8 +687,11 @@ function reloadRowHtml(r, thumbs) {
|
|
|
690
687
|
export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
691
688
|
const timingHtml = (group) => {
|
|
692
689
|
const record = group.record;
|
|
690
|
+
// Numbered by its place in the turn records, as live (a failed turn
|
|
691
|
+
// whose prompt went back still counts).
|
|
692
|
+
const at = (timing?.turnRecords || []).indexOf(record);
|
|
693
693
|
return record && formatDuration(record.duration_ms)
|
|
694
|
-
? `<div class="turn-timing">${escapeHtml(turnTimingText(group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" }))}</div>`
|
|
694
|
+
? `<div class="turn-timing">${escapeHtml(turnTimingText(at >= 0 ? at + 1 : group.turnIndex + 1, record.duration_ms, { canceled: record.status === "canceled" }))}</div>`
|
|
695
695
|
: "";
|
|
696
696
|
};
|
|
697
697
|
// The timing line after the bubble, not in it: where the live view
|
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.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -301,6 +301,7 @@ files:
|
|
|
301
301
|
- lib/samagotchi/plugin/sessions.rb
|
|
302
302
|
- lib/samagotchi/plugin/side_question.rb
|
|
303
303
|
- lib/samagotchi/plugin/tool_result.rb
|
|
304
|
+
- lib/samagotchi/plugin_session_state.rb
|
|
304
305
|
- lib/samagotchi/plugin_tasks.rb
|
|
305
306
|
- lib/samagotchi/project_scope.rb
|
|
306
307
|
- lib/samagotchi/prompt.rb
|
|
@@ -394,6 +395,7 @@ files:
|
|
|
394
395
|
- lib/samagotchi/update_command.rb
|
|
395
396
|
- lib/samagotchi/update_hint.rb
|
|
396
397
|
- lib/samagotchi/used_memories.rb
|
|
398
|
+
- lib/samagotchi/utf8_default.rb
|
|
397
399
|
- lib/samagotchi/version.rb
|
|
398
400
|
- lib/samagotchi/vision_context.rb
|
|
399
401
|
- lib/samagotchi/vision_support.rb
|