samagotchi 0.25.0 → 0.27.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 +47 -1
- data/docs/architecture.md +6 -0
- data/docs/configuration.md +1 -0
- data/docs/hooks.md +1 -1
- data/docs/internals/engine.md +478 -0
- data/docs/internals/prompt-caching.md +33 -7
- data/docs/plugins.md +3 -2
- data/lib/samagotchi/bridge.rb +37 -1
- data/lib/samagotchi/bridge_client.rb +9 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -5
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cancellation_controller.rb +8 -0
- data/lib/samagotchi/client.rb +53 -3
- data/lib/samagotchi/config.rb +9 -2
- data/lib/samagotchi/engine.rb +50 -7
- data/lib/samagotchi/kernel_loop.rb +39 -3
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/llm/http.rb +5 -4
- data/lib/samagotchi/log.rb +19 -3
- data/lib/samagotchi/prompt_warmup.rb +95 -0
- data/lib/samagotchi/terminal_ui.rb +2 -0
- data/lib/samagotchi/thinking_tails.rb +4 -3
- data/lib/samagotchi/tool_activity.rb +3 -2
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_view.rb +14 -3
- data/lib/samagotchi/tools/task_get.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +49 -12
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +22 -10
- data/lib/samagotchi/turn_note.rb +28 -2
- data/lib/samagotchi/turn_state.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +25 -0
- data/lib/samagotchi/web/markdown_renderer.rb +21 -6
- data/lib/samagotchi/web/public/app.js +4 -0
- data/lib/samagotchi/web/public/command_view.js +6 -3
- data/lib/samagotchi/web/public/data.js +11 -0
- data/lib/samagotchi/web/public/index.html +39 -13
- data/lib/samagotchi/web/public/stage_model.js +8 -2
- data/lib/samagotchi/web/public/stage_view.js +14 -2
- data/lib/samagotchi/web/public/task_stop.js +101 -0
- data/lib/samagotchi/web/public/turn_view.js +24 -1
- data/lib/samagotchi/worker.rb +3 -0
- metadata +4 -1
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
<title>Chi</title>
|
|
6
6
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
7
|
<style>
|
|
8
|
-
:root{
|
|
8
|
+
:root{color-scheme:dark;--bg:#0b0d11;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#7aa5ff;--accent-strong:#1f6feb;--accent-bg:#1b2336;--accent-ring:rgba(122,165,255,.18);--primary:#2a68d1;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#223150;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);--narr:color-mix(in srgb,var(--accent) 38%,var(--text-muted));
|
|
9
9
|
--glass-bg:color-mix(in srgb,var(--bg-panel) 70%,transparent);--glass-border:rgba(255,255,255,.08);--glass-highlight:rgba(255,255,255,.05);--glass-shadow:0 8px 30px rgba(0,0,0,.35);
|
|
10
|
-
--diff-add-bg:rgba(63,185,80,.14);--diff-add-text:#aff5b4;--diff-del-bg:rgba(248,81,73,.14);--diff-del-text:#ffc1bc
|
|
10
|
+
--diff-add-bg:rgba(63,185,80,.14);--diff-add-text:#aff5b4;--diff-del-bg:rgba(248,81,73,.14);--diff-del-text:#ffc1bc;
|
|
11
|
+
--code-text:#c9d1d9;--code-keyword:#ff7b72;--code-string:#a5d6ff;--code-comment:#8b949e;--code-constant:#79c0ff;--code-func:#d2a8ff;--code-type:#ffa657;
|
|
12
|
+
--card-hover-border:rgba(255,255,255,.14);--on-solid:#fff;--on-warn:#fff;--text-hover:#fff;--badge-bg:rgba(255,255,255,.08);--badge-ring:rgba(255,255,255,.18);--shadow-strong:rgba(0,0,0,.5);--backdrop:rgba(0,0,0,.85);--toast-shadow:0 6px 24px rgba(0,0,0,.25)}
|
|
13
|
+
/* Light palette: follows the system setting. :not([data-theme="dark"]) leaves room for a later in-page toggle (data-theme on <html>); there is none yet. Each palette sets color-scheme, so native radios, inputs and scrollbars follow it. */
|
|
14
|
+
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){color-scheme:light;--bg:#f5f6f8;--bg-panel:#ffffff;--bg-inset:#f1f3f6;--bg-raised:#e8ebf0;--bg-hover:#eceff4;--border:#dde1e7;--border-strong:#c3c9d2;--text:#1f2328;--text-bright:#0d1117;--text-2:#2f363d;--text-3:#475160;--text-muted:#57606a;--text-faint:#636c76;--accent:#0b5cd5;--accent-strong:#0550ae;--accent-bg:#e2ecfd;--accent-ring:rgba(11,92,213,.2);--primary:#1f6feb;--ok:#1a7f37;--ok-text:#116329;--ok-bg:#dafbe1;--warn:#9a6700;--warn-text:#7d4e00;--warn-bright:#875c00;--warn-bg:#fff6cc;--warn-border:#d4a72c;--danger:#cf222e;--danger-bg:#cf222e;--danger-text:#a40e26;--danger-bg-dim:#ffebe9;--think:#8250df;--think-text:#6639ba;--accent-border:#b8cef5;
|
|
15
|
+
--glass-border:rgba(31,35,40,.10);--glass-highlight:rgba(255,255,255,.7);--glass-shadow:0 8px 30px rgba(31,35,40,.10);
|
|
16
|
+
--diff-add-bg:rgba(26,127,55,.12);--diff-add-text:#116329;--diff-del-bg:rgba(207,34,46,.10);--diff-del-text:#82071e;
|
|
17
|
+
--code-text:#1f2328;--code-keyword:#cf222e;--code-string:#0a3069;--code-comment:#57606a;--code-constant:#0550ae;--code-func:#8250df;--code-type:#953800;
|
|
18
|
+
--card-hover-border:rgba(31,35,40,.18);--on-solid:#fff;--on-warn:#1f2328;--text-hover:#0d1117;--badge-bg:rgba(31,35,40,.06);--badge-ring:rgba(31,35,40,.15);--shadow-strong:rgba(31,35,40,.18);--backdrop:rgba(13,17,23,.85);--toast-shadow:0 6px 24px rgba(31,35,40,.14)}}
|
|
11
19
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
12
20
|
body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:radial-gradient(1100px 700px at 8% 0%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 70%),radial-gradient(900px 700px at 100% 100%,color-mix(in srgb,var(--think) 9%,transparent),transparent 70%),var(--bg);background-attachment:fixed;color:var(--text);height:100vh;display:flex;flex-direction:column}
|
|
13
21
|
/* Glass: the composer card, the strip cards, the all-sessions cards and the question cards (by selector, so app.js is untouched). */
|
|
14
22
|
#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
|
-
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:
|
|
23
|
+
#topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:var(--card-hover-border)}
|
|
16
24
|
#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
25
|
/* 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
26
|
#topStrip .card.waiting,#allList .card.waiting{border-color:color-mix(in srgb,var(--warn) 75%,transparent)}
|
|
@@ -23,7 +31,7 @@ body.no-glass #dock,body.no-glass .turn-stage,body.no-glass .dock-card,body.no-g
|
|
|
23
31
|
body.no-glass .bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
24
32
|
body.no-glass .bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
|
|
25
33
|
button,input,textarea,select{font:inherit}
|
|
26
|
-
button{background:var(--primary);border:0;color
|
|
34
|
+
button{background:var(--primary);border:0;color:var(--on-solid);padding:8px 12px;border-radius:8px;cursor:pointer;transition:filter .12s ease,background .12s ease}
|
|
27
35
|
button:hover:not(:disabled){filter:brightness(1.12)}
|
|
28
36
|
button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
29
37
|
button.ghost{background:var(--bg-raised);color:var(--text-2)}
|
|
@@ -56,6 +64,18 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
56
64
|
.bubble.output.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
57
65
|
.bubble.output.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:1px 5px;color:var(--text)}
|
|
58
66
|
.bubble.output.markdown pre{font-size:13px;line-height:1.45;border:1px solid var(--border)}
|
|
67
|
+
/* Code highlighting (markdown_renderer.rb): syntect scope classes, hl-prefixed, coloured from the palette's --code-* tokens. More classes win over fewer (a function name is also "entity"). */
|
|
68
|
+
.syntax-highlighting{color:var(--code-text)}
|
|
69
|
+
.syntax-highlighting .hl-keyword,.syntax-highlighting .hl-storage{color:var(--code-keyword)}
|
|
70
|
+
.syntax-highlighting .hl-string{color:var(--code-string)}
|
|
71
|
+
.syntax-highlighting .hl-string .hl-embedded{color:var(--code-text)}
|
|
72
|
+
.syntax-highlighting .hl-constant{color:var(--code-constant)}
|
|
73
|
+
.syntax-highlighting .hl-entity.hl-name.hl-function,.syntax-highlighting .hl-support.hl-function{color:var(--code-func)}
|
|
74
|
+
.syntax-highlighting .hl-entity.hl-name.hl-class,.syntax-highlighting .hl-entity.hl-name.hl-type,.syntax-highlighting .hl-entity.hl-other.hl-inherited-class,.syntax-highlighting .hl-support.hl-class,.syntax-highlighting .hl-support.hl-type,.syntax-highlighting .hl-meta.hl-diff.hl-range{color:var(--code-type)}
|
|
75
|
+
.syntax-highlighting .hl-comment{color:var(--code-comment);font-style:italic}
|
|
76
|
+
.syntax-highlighting .hl-markup.hl-inserted{color:var(--diff-add-text)}
|
|
77
|
+
.syntax-highlighting .hl-markup.hl-deleted{color:var(--diff-del-text)}
|
|
78
|
+
.syntax-highlighting .hl-invalid{color:var(--danger-text)}
|
|
59
79
|
.bubble.output.markdown h1,.bubble.output.markdown h2,.bubble.output.markdown h3,.bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6{font-weight:600;line-height:1.3;margin:18px 0 8px;color:var(--text-bright)}
|
|
60
80
|
.bubble.output.markdown h1{font-size:19px}
|
|
61
81
|
.bubble.output.markdown h2{font-size:17px}
|
|
@@ -91,8 +111,8 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
91
111
|
.bubble.user.queued{border-left-style:dashed;opacity:.8}
|
|
92
112
|
.bubble.user.steered{border-left-style:dashed;border-left-color:var(--accent);opacity:.9}
|
|
93
113
|
.bubble.user.failed{border-left-color:var(--danger);opacity:.8}
|
|
94
|
-
.bubble.user .state-badge{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:2px 7px;border-radius:999px;background:
|
|
95
|
-
.bubble.user .state-badge::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.8;box-shadow:0 0 0 1px
|
|
114
|
+
.bubble.user .state-badge{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:2px 7px;border-radius:999px;background:var(--badge-bg);color:var(--text);font-size:11px;line-height:1.2;white-space:nowrap;flex-shrink:0}
|
|
115
|
+
.bubble.user .state-badge::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.8;box-shadow:0 0 0 1px var(--badge-ring)}
|
|
96
116
|
.bubble.user.queued .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
|
|
97
117
|
.bubble.user.steered .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
|
|
98
118
|
.bubble.user.failed .state-badge::before{background:var(--danger);box-shadow:0 0 0 1px color-mix(in srgb,var(--danger) 40%,transparent)}
|
|
@@ -116,6 +136,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
116
136
|
.activity-status.stopped{color:var(--warn-text);opacity:.8}
|
|
117
137
|
.activity-status.blocked{color:var(--danger-text)}
|
|
118
138
|
.activity-tool{color:var(--accent);font-weight:600}
|
|
139
|
+
/* The stop-task button on a running task_wait (task_stop.js): a small ghost
|
|
140
|
+
button in the danger colours, at the row's end and in the stage's now slot. */
|
|
141
|
+
button.task-stop{flex:none;margin-left:auto;padding:1px 8px;border:1px solid color-mix(in srgb,var(--danger) 45%,transparent);border-radius:6px;background:transparent;color:var(--danger-text);font-size:11.5px;line-height:18px;white-space:nowrap}
|
|
142
|
+
button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:none}
|
|
143
|
+
.ts-now button.task-stop{align-self:center}
|
|
119
144
|
.activity-params{color:var(--text-muted);word-break:break-all}
|
|
120
145
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
121
146
|
.activity-output{margin:4px 0 2px 60px;font-size:12px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;background:var(--bg);border-radius:6px;padding:6px 8px;max-height:160px;overflow:auto}
|
|
@@ -145,6 +170,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
145
170
|
/* The command as its steps (CommandSteps): the op that links each to the one before, labels as small headings, limits and heredocs as chips; "raw" swaps in the command as written (CSS only). */
|
|
146
171
|
.activity-command.has-steps{background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;padding:4px 8px 5px}
|
|
147
172
|
.activity-command-head{display:flex;align-items:center;gap:8px;min-height:20px;padding-right:30px}
|
|
173
|
+
.activity-command-head.bare{float:right;min-height:0;margin-left:8px}
|
|
148
174
|
.has-steps .activity-command-cwd{flex:1 1 auto;min-width:0;margin:0;overflow-wrap:anywhere}
|
|
149
175
|
.activity-command-cwd>.activity-command-sep{font-family:inherit;color:var(--text-faint)}
|
|
150
176
|
.activity-command-raw{flex:none;display:inline-flex;align-items:center;height:18px;padding:0 7px;border:1px solid var(--border-strong);border-radius:9px;font-size:11px;color:var(--text-muted);cursor:pointer;user-select:none;-webkit-user-select:none}
|
|
@@ -366,7 +392,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
366
392
|
/* The selection bubble (app.js Annotate): Annotate + the presets (web.annotate_presets), small pills with a gap (they wrap on a narrow page). */
|
|
367
393
|
#annotateBar{position:fixed;z-index:20;display:flex;flex-wrap:wrap;gap:4px;max-width:calc(100vw - 16px)}
|
|
368
394
|
#annotateBar[hidden]{display:none}
|
|
369
|
-
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px
|
|
395
|
+
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px var(--shadow-strong)}
|
|
370
396
|
#annotateBar .annotate-preset{background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong);padding:3px 10px;max-width:32ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
371
397
|
/* Copy buttons (copy.js): top-right in an answer and a code block, left of a prompt (its right edge is the page's); shown on hover or focus, always (dim) on touch. They hold no text: "copied ✓" is CSS content. */
|
|
372
398
|
.bubble.output,.bubble.user{position:relative}
|
|
@@ -407,7 +433,7 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
407
433
|
.thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
408
434
|
.thumb{max-height:120px;max-width:200px;border-radius:6px;border:1px solid var(--border-strong);cursor:zoom-in;object-fit:contain;background:var(--bg)}
|
|
409
435
|
.activity-row .thumbs{flex-basis:100%}
|
|
410
|
-
#lightbox{position:fixed;inset:0;z-index:50;background:
|
|
436
|
+
#lightbox{position:fixed;inset:0;z-index:50;background:var(--backdrop);display:flex;align-items:center;justify-content:center;cursor:zoom-out}
|
|
411
437
|
#lightbox[hidden]{display:none}
|
|
412
438
|
#lightbox img{max-width:96vw;max-height:94vh;border-radius:6px}
|
|
413
439
|
#composer textarea{flex:1 1 auto;min-height:72px;resize:none;background:transparent;border:0;color:var(--text);padding:2px;border-radius:0;font-size:15px;line-height:1.45}
|
|
@@ -598,7 +624,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
598
624
|
#infoBar .meta{font-size:11px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
|
599
625
|
#infoBar .meta .id{font-family:ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--accent)}
|
|
600
626
|
#infoBar .meta .copy-attach{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong)}
|
|
601
|
-
#infoBar .meta .copy-attach:hover{color
|
|
627
|
+
#infoBar .meta .copy-attach:hover{color:var(--text-hover);border-color:var(--border-strong)}
|
|
602
628
|
#infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
|
|
603
629
|
#infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);word-break:break-all}
|
|
604
630
|
/* The footer sits on the card's glass: its chips are tinted, not solid, so the glass reads through them. */
|
|
@@ -610,7 +636,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
610
636
|
#infoBar .meta .worker-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:2px 2px 2px 6px;border-radius:6px;background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border);white-space:nowrap}
|
|
611
637
|
#infoBar .meta .worker-badge:not(:has(button)){padding-right:6px}
|
|
612
638
|
#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)}
|
|
613
|
-
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color
|
|
639
|
+
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}
|
|
614
640
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
615
641
|
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;line-height:1.4;max-height:2.8em}
|
|
616
642
|
#infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
|
|
@@ -621,7 +647,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
621
647
|
#infoBar #infoArchiveBtn{background:transparent;color:var(--text-muted);border:1px solid var(--border-strong)}
|
|
622
648
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
623
649
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
624
|
-
#infoBar #infoDeleteBtn:hover{color
|
|
650
|
+
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
625
651
|
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable) and the first-message line, the strip card's title, goes. */
|
|
626
652
|
@media(max-width:600px){
|
|
627
653
|
/* The key hint: cut off at this width, and phones have no Shift+Enter. */
|
|
@@ -743,7 +769,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
743
769
|
.card.archived{opacity:.6}
|
|
744
770
|
.card.archived:hover{opacity:.85}
|
|
745
771
|
.card .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
746
|
-
#toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:
|
|
772
|
+
#toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
747
773
|
#toast[hidden]{display:none}
|
|
748
774
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
749
775
|
#toast .toast-close{background:transparent;border:0;color:var(--text-muted);cursor:pointer;padding:2px 4px;margin-left:-6px}
|
|
@@ -761,7 +787,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
761
787
|
#allList .card{position:relative}
|
|
762
788
|
.card-del{position:absolute;top:6px;right:6px;padding:2px 7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);font-size:12px;line-height:1.2;cursor:pointer;opacity:0;transition:opacity .15s}
|
|
763
789
|
#allList .card:hover .card-del, .card-del:focus-visible{opacity:1}
|
|
764
|
-
.card-del:hover{color
|
|
790
|
+
.card-del:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
765
791
|
@media (hover:none){.card-del{opacity:1}}
|
|
766
792
|
.card.active{background:var(--accent-bg);border-color:var(--accent-border)}
|
|
767
793
|
.card .preview{font-size:14px;line-height:1.4;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -85,6 +85,12 @@ function withCommand(slot, row) {
|
|
|
85
85
|
return row.view?.command ? { ...slot, command: row.view.command } : slot;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// A running task_wait's slot: the task it waits on and the row's key, for
|
|
89
|
+
// the stop-task button (task_stop.js).
|
|
90
|
+
function withTask(slot, row) {
|
|
91
|
+
return row.tool === "task_wait" && row.view?.task_id ? { ...slot, taskId: row.view.task_id, key: row.key } : slot;
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
// While the first step has no thinking and no tool rows the turn is plain:
|
|
89
95
|
// its text streams in the stage's answer slot, as the turn view's
|
|
90
96
|
// chrome-less block streams like a bubble.
|
|
@@ -106,7 +112,7 @@ const NO_ANSWER = "no answer";
|
|
|
106
112
|
// feed), else the newest thinking one (+thinking+, its ticker),
|
|
107
113
|
// else "…";
|
|
108
114
|
// tool the running call ({name, title, kind}, and command: its full
|
|
109
|
-
// command when it has one) or null;
|
|
115
|
+
// command when it has one; taskId and key: a task_wait's) or null;
|
|
110
116
|
// trail the last three finished calls, newest last (a file's title
|
|
111
117
|
// without its dir; command as for tool);
|
|
112
118
|
// ticks one per call: {kind, status, step (the gen's index)}.
|
|
@@ -137,7 +143,7 @@ export function liveSlots(turn, { narration = "", thinking = "", pending = false
|
|
|
137
143
|
phase,
|
|
138
144
|
step: ended ? gens.filter((g) => !g.answer || g.thinking || g.tools.length).length : gens.length,
|
|
139
145
|
headline,
|
|
140
|
-
tool: running ? withCommand({ name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) }, running) : null,
|
|
146
|
+
tool: running ? withTask(withCommand({ name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) }, running), running) : null,
|
|
141
147
|
trail,
|
|
142
148
|
ticks: rows.map(({ row, step }) => ({ kind: toolKind(row.tool), status: row.status, step })),
|
|
143
149
|
};
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { createTurnView } from "./turn_view.js";
|
|
14
14
|
import { flashOf, handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor } from "./stage_model.js";
|
|
15
15
|
import { formatDuration } from "./timing.js";
|
|
16
|
+
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
16
17
|
|
|
17
18
|
const COLLAPSE_KEY = "chi_stage_collapsed";
|
|
18
19
|
const WATCH_MS = 200;
|
|
@@ -45,7 +46,9 @@ function readCollapsed() {
|
|
|
45
46
|
// +historyNearBottom+: the history's own follow check (the hand-off scrolls
|
|
46
47
|
// the history only for a reader at its end). +dockEl+, +centerEl+ and
|
|
47
48
|
// +composerEl+ size the stage's cap.
|
|
48
|
-
|
|
49
|
+
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
50
|
+
// on a running task_wait.
|
|
51
|
+
export function createStageView({ historyEl, appendToHistory, isNearBottom: historyNearBottom, sawText, sessionId, dockEl, centerEl, composerEl, taskStop = null }) {
|
|
49
52
|
// The dock becomes a clear column: the stage's card, then the composer's
|
|
50
53
|
// (its rows moved into .dock-card, which wears the glass and the focus ring).
|
|
51
54
|
document.body.classList.add("stage-view");
|
|
@@ -131,6 +134,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
131
134
|
},
|
|
132
135
|
useHold: false,
|
|
133
136
|
newestFirst: true,
|
|
137
|
+
taskStop,
|
|
134
138
|
onChange: () => scheduleRefresh(),
|
|
135
139
|
onThinkingLine: (text) => {
|
|
136
140
|
const { turn } = view.current();
|
|
@@ -223,7 +227,11 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
223
227
|
|
|
224
228
|
function drawNow(tool, gen) {
|
|
225
229
|
const quiet = gen?.text && !gen.textDone ? "writing…" : "thinking…";
|
|
226
|
-
|
|
230
|
+
// A task_wait's stop button, as the row's (task_stop.js): in the key
|
|
231
|
+
// with its in-flight state, so a redraw keeps a clicked one disabled.
|
|
232
|
+
const stopId = tool?.taskId && taskStop?.target({ tool: "task_wait", status: "running", view: { task_id: tool.taskId } });
|
|
233
|
+
const stopKey = stopId ? `|stop:${tool.key}:${taskStop.busy(stopId)}` : "";
|
|
234
|
+
const key = tool ? `run|${tool.name}|${tool.title}|${tool.command || ""}${stopKey}` : `quiet|${quiet}`;
|
|
227
235
|
if (shown.now === key) return;
|
|
228
236
|
shown.now = key;
|
|
229
237
|
now.replaceChildren();
|
|
@@ -236,6 +244,10 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
236
244
|
title.textContent = tool.title;
|
|
237
245
|
// The full command when the call has one (the title is its first step +N).
|
|
238
246
|
title.title = tool.command || tool.title;
|
|
247
|
+
if (stopId) {
|
|
248
|
+
const command = () => taskCommand(view.current().turn?.gens, stopId);
|
|
249
|
+
now.appendChild(taskStopButton(taskStop, stopId, { command, onChange: scheduleRefresh }));
|
|
250
|
+
}
|
|
239
251
|
} else {
|
|
240
252
|
const word = node("span", "ts-title quiet", now);
|
|
241
253
|
word.textContent = quiet;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The stop-task button on a running task_wait (POST …/tasks/:task_id/stop,
|
|
2
|
+
// Bridge#handle_task_stop): the worker kills the task as the user, the wait
|
|
3
|
+
// returns, and the turn goes on. Which row gets one, what the confirm says,
|
|
4
|
+
// and the stops in flight. The in-flight set lives here, not in the DOM:
|
|
5
|
+
// the stage rebuilds its "now" slot, and a redraw must keep a clicked
|
|
6
|
+
// button disabled. Pure but for the injected calls, so `node --test`
|
|
7
|
+
// covers it; turn_view.js and stage_view.js draw the button.
|
|
8
|
+
import { stopTask as postStopTask } from "./data.js";
|
|
9
|
+
|
|
10
|
+
// What a worker that has the route names in its sidecar features.
|
|
11
|
+
export const TASK_STOP_FEATURE = "task_stop";
|
|
12
|
+
const COMMAND_SHOWN = 120;
|
|
13
|
+
|
|
14
|
+
// The task a row's button would stop: a running task_wait's (its server
|
|
15
|
+
// view's task_id), on a worker that has the route; else null.
|
|
16
|
+
export function stopTarget(row, features) {
|
|
17
|
+
if (!row || row.tool !== "task_wait" || row.status !== "running") return null;
|
|
18
|
+
if (!Array.isArray(features) || !features.includes(TASK_STOP_FEATURE)) return null;
|
|
19
|
+
return row.view?.task_id || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The command a task_create of this turn started +taskId+ with (its row's
|
|
23
|
+
// view), or null: a task from an earlier turn has no row here.
|
|
24
|
+
export function taskCommand(gens, taskId) {
|
|
25
|
+
for (const gen of gens || []) {
|
|
26
|
+
for (const row of gen.tools || []) {
|
|
27
|
+
if (row.tool !== "task_create") continue;
|
|
28
|
+
const id = /(?:^|\n)task_id: (\S+)/.exec(row.output || "")?.[1];
|
|
29
|
+
if (id === taskId) return row.view?.command || null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function confirmText(taskId, command) {
|
|
36
|
+
const line = String(command || "").replace(/\s+/g, " ").trim();
|
|
37
|
+
const what = !line ? taskId : line.length > COMMAND_SHOWN ? `${line.slice(0, COMMAND_SHOWN - 1)}…` : line;
|
|
38
|
+
return `Stop task ${what}?`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A failed stop said nothing about: 409 (the task ended meanwhile), 503
|
|
42
|
+
// (no live worker: it can't be stopped now).
|
|
43
|
+
export function quietFailure(error) {
|
|
44
|
+
return error?.status === 409 || error?.status === 503;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// +sessionId+ and +features+ are read at each call (the selected session,
|
|
48
|
+
// its worker's sidecar features).
|
|
49
|
+
export function createTaskStopper({
|
|
50
|
+
sessionId,
|
|
51
|
+
features,
|
|
52
|
+
stopTask = postStopTask,
|
|
53
|
+
confirm = (text) => globalThis.confirm(text),
|
|
54
|
+
alert = (text) => globalThis.alert(text),
|
|
55
|
+
}) {
|
|
56
|
+
const inFlight = new Set();
|
|
57
|
+
const keyOf = (taskId) => `${sessionId()}|${taskId}`;
|
|
58
|
+
return {
|
|
59
|
+
target: (row) => stopTarget(row, features()),
|
|
60
|
+
busy: (taskId) => inFlight.has(keyOf(taskId)),
|
|
61
|
+
// true once the worker stopped it. A stop that went through stays in
|
|
62
|
+
// flight: the button goes with its row's completion a moment later.
|
|
63
|
+
async stop(taskId, command = null) {
|
|
64
|
+
const id = sessionId();
|
|
65
|
+
const key = keyOf(taskId);
|
|
66
|
+
if (!id || inFlight.has(key) || !confirm(confirmText(taskId, command))) return false;
|
|
67
|
+
inFlight.add(key);
|
|
68
|
+
try {
|
|
69
|
+
await stopTask(id, taskId);
|
|
70
|
+
return true;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
inFlight.delete(key);
|
|
73
|
+
if (!quietFailure(e)) alert(e.message);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The button itself (the one DOM part): a click asks, posts, and keeps it
|
|
81
|
+
// disabled while the stop is in flight. +command+ is read at the click;
|
|
82
|
+
// +onChange+ lets a view redraw (the stage's slot).
|
|
83
|
+
export function taskStopButton(stopper, taskId, { command = () => null, onChange = () => {} } = {}) {
|
|
84
|
+
const btn = document.createElement("button");
|
|
85
|
+
btn.type = "button";
|
|
86
|
+
btn.className = "ghost task-stop";
|
|
87
|
+
btn.textContent = "stop task";
|
|
88
|
+
btn.title = "Kill this background task; the model is told you stopped it";
|
|
89
|
+
btn.disabled = stopper.busy(taskId);
|
|
90
|
+
btn.addEventListener("click", (e) => {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
const pending = stopper.stop(taskId, command());
|
|
93
|
+
btn.disabled = stopper.busy(taskId);
|
|
94
|
+
onChange();
|
|
95
|
+
pending.then(() => {
|
|
96
|
+
btn.disabled = stopper.busy(taskId);
|
|
97
|
+
onChange();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return btn;
|
|
101
|
+
}
|
|
@@ -23,6 +23,7 @@ import { createSentenceFeed } from "./sentences.js";
|
|
|
23
23
|
import { createTicker } from "./thinking_ticker.js";
|
|
24
24
|
import * as model from "./turn_model.js";
|
|
25
25
|
import { createHold } from "./hold.js";
|
|
26
|
+
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
26
27
|
|
|
27
28
|
// A row's status (live, or a saved tool record's on reload): running,
|
|
28
29
|
// error, stopped (a task_wait the user's Stop ended), blocked (a guardrail
|
|
@@ -90,7 +91,9 @@ function fillActivityRow(el, row, sessionId) {
|
|
|
90
91
|
// (chronological() restores the order);
|
|
91
92
|
// onChange() after anything in the turn changed (the stage redraws
|
|
92
93
|
// its slots from current());
|
|
93
|
-
// onThinkingLine(sentence) the live step's ticker showed a new line
|
|
94
|
+
// onThinkingLine(sentence) the live step's ticker showed a new line;
|
|
95
|
+
// taskStop the page's stopper (task_stop.js): a running task_wait
|
|
96
|
+
// row gets its stop-task button (none: no button).
|
|
94
97
|
export function createTurnView({
|
|
95
98
|
historyEl, appendToHistory, isNearBottom, sawText, sessionId,
|
|
96
99
|
blockHost = appendToHistory,
|
|
@@ -100,6 +103,7 @@ export function createTurnView({
|
|
|
100
103
|
newestFirst = false,
|
|
101
104
|
onChange = () => {},
|
|
102
105
|
onThinkingLine = () => {},
|
|
106
|
+
taskStop = null,
|
|
103
107
|
}) {
|
|
104
108
|
let turn = null;
|
|
105
109
|
let blockEl = null;
|
|
@@ -407,14 +411,33 @@ export function createTurnView({
|
|
|
407
411
|
const stick = isNearBottom();
|
|
408
412
|
body.appendChild(el);
|
|
409
413
|
fillActivityRow(el, r.row, sessionId());
|
|
414
|
+
syncTaskStop(el, r.row);
|
|
410
415
|
if (stick) follow();
|
|
411
416
|
} else {
|
|
412
417
|
fillActivityRow(el, r.row, sessionId());
|
|
418
|
+
syncTaskStop(el, r.row);
|
|
413
419
|
}
|
|
414
420
|
updateBlockSummary();
|
|
415
421
|
onChange();
|
|
416
422
|
}
|
|
417
423
|
|
|
424
|
+
// The stop-task button after a running task_wait's params; gone once the
|
|
425
|
+
// row completes.
|
|
426
|
+
function syncTaskStop(el, row) {
|
|
427
|
+
const taskId = taskStop?.target(row);
|
|
428
|
+
const btn = el.querySelector(":scope > .task-stop");
|
|
429
|
+
if (!taskId) {
|
|
430
|
+
btn?.remove();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (btn) {
|
|
434
|
+
btn.disabled = taskStop.busy(taskId);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const command = () => taskCommand(turn?.gens, taskId);
|
|
438
|
+
el.querySelector(".activity-params").after(taskStopButton(taskStop, taskId, { command }));
|
|
439
|
+
}
|
|
440
|
+
|
|
418
441
|
// A hook's line (hook_notice) as a row of the current step, above the row
|
|
419
442
|
// of the call it judged; false with no running turn (app.js shows the
|
|
420
443
|
// bubble then). data.line is a whole row of chi's own (an empty-answer
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -112,6 +112,9 @@ module Samagotchi
|
|
|
112
112
|
# stream gets the session's history and status in its snapshot, not
|
|
113
113
|
# an empty session until the first turn.
|
|
114
114
|
@engine.session = @session
|
|
115
|
+
# Input already waiting in the inbox starts the next turn at once: no
|
|
116
|
+
# turn-end warm-up then.
|
|
117
|
+
@engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? }
|
|
115
118
|
@turn_flow = TurnFlow.new(engine: @engine)
|
|
116
119
|
# Its question's answer is queued as the matching /continue (the
|
|
117
120
|
# Bridge exists by the time anyone answers).
|
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.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -130,6 +130,7 @@ files:
|
|
|
130
130
|
- docs/hooks.md
|
|
131
131
|
- docs/internals/background-tasks.md
|
|
132
132
|
- docs/internals/context-telemetry.md
|
|
133
|
+
- docs/internals/engine.md
|
|
133
134
|
- docs/internals/gemma4-contract.md
|
|
134
135
|
- docs/internals/prompt-caching.md
|
|
135
136
|
- docs/internals/tool-guardrails.md
|
|
@@ -324,6 +325,7 @@ files:
|
|
|
324
325
|
- lib/samagotchi/prompt.rb
|
|
325
326
|
- lib/samagotchi/prompt_history.rb
|
|
326
327
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
328
|
+
- lib/samagotchi/prompt_warmup.rb
|
|
327
329
|
- lib/samagotchi/question_desk.rb
|
|
328
330
|
- lib/samagotchi/recap_setup.rb
|
|
329
331
|
- lib/samagotchi/recap_store.rb
|
|
@@ -469,6 +471,7 @@ files:
|
|
|
469
471
|
- lib/samagotchi/web/public/stage_view.js
|
|
470
472
|
- lib/samagotchi/web/public/strip.js
|
|
471
473
|
- lib/samagotchi/web/public/tally.js
|
|
474
|
+
- lib/samagotchi/web/public/task_stop.js
|
|
472
475
|
- lib/samagotchi/web/public/thinking_ticker.js
|
|
473
476
|
- lib/samagotchi/web/public/timing.js
|
|
474
477
|
- lib/samagotchi/web/public/turn_events.js
|