samagotchi 0.120.0 → 0.121.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 +35 -1
- data/docs/configuration.md +29 -10
- data/docs/internals/participants.md +3 -1
- data/docs/internals/prompt-caching.md +7 -6
- data/docs/plugins.md +24 -8
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +5 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/kernel_loop.rb +5 -0
- data/lib/samagotchi/plugin/api.rb +6 -3
- data/lib/samagotchi/terminal_ui/question_slot.rb +19 -8
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/toolkit.rb +50 -26
- data/lib/samagotchi/toolkit_binding.rb +32 -0
- data/lib/samagotchi/toolkit_command.rb +23 -0
- data/lib/samagotchi/tools/deferred_tools.rb +10 -2
- data/lib/samagotchi/tools/registry.rb +20 -6
- data/lib/samagotchi/tools/tool_index.rb +103 -20
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/participant_review.rb +9 -5
- data/lib/samagotchi/web/public/forms.css +12 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/level_chip.js +2 -2
- data/lib/samagotchi/web/public/llm_context_chip.js +4 -4
- data/lib/samagotchi/web/public/participant.html +1 -0
- data/lib/samagotchi/web/public/participant.js +13 -4
- data/lib/samagotchi/web/public/participants_panel.js +3 -3
- data/lib/samagotchi/web/public/question_card.js +15 -1
- data/lib/samagotchi/web/public/question_cards.js +11 -2
- data/lib/samagotchi/web/public/review_history.js +10 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -0
- data/lib/samagotchi/web/tunnel.rb +10 -1
- metadata +2 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* A form's select box (participants_panel.js' Invite, level_chip.js' Level, llm_context_chip.js' Strategy / Apply):
|
|
2
|
+
one shape in every dialog. Each browser draws its own closed box (macOS a pill with a double chevron), so a select
|
|
3
|
+
in a .select-wrap drops it and keeps its dialog's text-field box (the dialog's own `input, select` rule: height,
|
|
4
|
+
radius, border, background, padding, font); the wrapper draws one thin caret at the right. The open list stays the
|
|
5
|
+
OS menu. Both pages link it, after tokens.css and before their own <style>: span.select-wrap>select outweighs a
|
|
6
|
+
dialog's `… select` rule, so its padding shorthand keeps room for the caret. Colours are tokens.css's. */
|
|
7
|
+
.select-wrap{position:relative;display:inline-flex;min-width:0}
|
|
8
|
+
span.select-wrap>select{appearance:none;-webkit-appearance:none;flex:1;min-width:0;padding-right:26px;cursor:pointer}
|
|
9
|
+
.select-wrap::after{content:"";position:absolute;right:11px;top:50%;width:5px;height:5px;border-right:1.5px solid var(--text-muted);border-bottom:1.5px solid var(--text-muted);transform:translateY(-70%) rotate(45deg);pointer-events:none}
|
|
10
|
+
.select-wrap select option{background:var(--bg-raised);color:var(--text)}
|
|
11
|
+
span.select-wrap>select:disabled{opacity:.6;cursor:default}
|
|
12
|
+
.select-wrap:has(select:disabled)::after{opacity:.6}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<link rel="stylesheet" href="/assets/tokens.css">
|
|
13
13
|
<link rel="stylesheet" href="/assets/annotate_bar.css">
|
|
14
14
|
<link rel="stylesheet" href="/assets/composer.css">
|
|
15
|
+
<link rel="stylesheet" href="/assets/forms.css">
|
|
15
16
|
<style>
|
|
16
17
|
/* Icon masks shared by the phone info bar and the all view's cards (the stroke only shapes the mask; the icon takes currentColor); the image icon is composer.css's. */
|
|
17
18
|
:root{--icon-archive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M6.5 9h3'/%3E%3C/svg%3E");--icon-unarchive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M8 11.5V8.25M6.5 9.75 8 8.25l1.5 1.5'/%3E%3C/svg%3E")}
|
|
@@ -425,6 +426,10 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
|
|
|
425
426
|
.question-option:hover{background:var(--bg-hover)}
|
|
426
427
|
.question-option.selected{border-color:var(--ok);color:var(--text)}
|
|
427
428
|
.question-option input{margin:0;accent-color:var(--warn)}
|
|
429
|
+
/* The model's recommended option (a label ending in "(Recommended)"): an accent row and a small tag. */
|
|
430
|
+
.question-option.recommended{border-color:var(--accent-border);background:var(--accent-bg);color:var(--text)}
|
|
431
|
+
.question-option.recommended.selected{border-color:var(--ok)}
|
|
432
|
+
.question-option-tag{margin-left:auto;flex:none;font-size:11px;color:var(--accent);border:1px solid var(--accent-border);border-radius:999px;padding:0 6px}
|
|
428
433
|
.question-freeform{width:100%;box-sizing:border-box;background:var(--bg-inset);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:8px;font-size:13px;margin-bottom:8px}
|
|
429
434
|
.question-error{color:var(--danger);font-size:12px;margin-bottom:8px}
|
|
430
435
|
.question-error.hidden{display:none}
|
|
@@ -1055,12 +1060,9 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1055
1060
|
.llmctx-field>span{flex:0 0 58px;font-size:12px;color:var(--text-muted)}
|
|
1056
1061
|
.llmctx-field select,.llmctx-field input{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;border-radius:7px;border:1px solid var(--border-strong);background:var(--bg-inset);color:var(--text);font:inherit;font-size:13px;line-height:1.4}
|
|
1057
1062
|
.llmctx-field select:focus,.llmctx-field input:focus{outline:none;border-color:var(--accent-border)}
|
|
1058
|
-
/*
|
|
1059
|
-
.llmctx-field
|
|
1060
|
-
.llmctx-field select option{background:var(--bg-raised);color:var(--text)}
|
|
1063
|
+
/* A select's closed box and caret: forms.css (.select-wrap); here the wrapper takes the field's width. */
|
|
1064
|
+
.llmctx-field>.select-wrap{flex:1}
|
|
1061
1065
|
.llmctx-field input::placeholder{color:var(--text-faint);opacity:1}
|
|
1062
|
-
.llmctx-field:has(select){position:relative}
|
|
1063
|
-
.llmctx-field:has(select)::after{content:"";position:absolute;right:11px;top:50%;width:5px;height:5px;border-right:1.5px solid var(--text-muted);border-bottom:1.5px solid var(--text-muted);transform:translateY(-70%) rotate(45deg);pointer-events:none}
|
|
1064
1066
|
.llmctx-form .llmctx-set{background:var(--accent-bg);color:var(--accent);border-color:var(--accent-border)}
|
|
1065
1067
|
.llmctx-form .llmctx-set:disabled{opacity:.6;cursor:default}
|
|
1066
1068
|
/* Attached context (context_chips.js): a row of its own under the meta; not the composer's #chips/.chip. */
|
|
@@ -69,8 +69,8 @@ export function formTemplate(cfg, summary, { running = false, kind = SESSION, ch
|
|
|
69
69
|
><span class="ctx-pop-kind">${fresh ? NEW_SESSION : "this session"}</span
|
|
70
70
|
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
71
71
|
><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
|
|
72
|
-
><label class="llmctx-field"><span>Level</span><select name="level">${option(DEFAULT, follow, chosen)}${
|
|
73
|
-
cfg.levels.map((level) => option(level, level, chosen))}</select></label
|
|
72
|
+
><label class="llmctx-field"><span>Level</span><span class="select-wrap"><select name="level">${option(DEFAULT, follow, chosen)}${
|
|
73
|
+
cfg.levels.map((level) => option(level, level, chosen))}</select></span></label
|
|
74
74
|
><div class="ctx-pop-meta">${cfg.when(fresh)}${running ? " A turn is running: it applies after this turn." : ""}</div
|
|
75
75
|
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
76
76
|
}
|
|
@@ -179,10 +179,10 @@ export function formTemplate(summary, { running = false, kind = SESSION, choice
|
|
|
179
179
|
><span class="ctx-pop-kind">${fresh ? NEW_SESSION : "this session"}</span
|
|
180
180
|
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
181
181
|
><div class="ctx-pop-meta llmctx-now">${nowText(now)}</div
|
|
182
|
-
><label class="llmctx-field"><span>Strategy</span><select name="strategy" .value=${chosen(own.strategy)}>${
|
|
183
|
-
option(DEFAULT, follow("strategy"))}${strategyOptions(own.strategy).map((s) => option(s.value, s.label))}</select></label
|
|
184
|
-
><label class="llmctx-field"><span>Apply</span><select name="apply" .value=${chosen(own.apply)}>${
|
|
185
|
-
option(DEFAULT, follow("apply"))}${APPLIES.map((a) => option(a, a))}</select></label
|
|
182
|
+
><label class="llmctx-field"><span>Strategy</span><span class="select-wrap"><select name="strategy" .value=${chosen(own.strategy)}>${
|
|
183
|
+
option(DEFAULT, follow("strategy"))}${strategyOptions(own.strategy).map((s) => option(s.value, s.label))}</select></span></label
|
|
184
|
+
><label class="llmctx-field"><span>Apply</span><span class="select-wrap"><select name="apply" .value=${chosen(own.apply)}>${
|
|
185
|
+
option(DEFAULT, follow("apply"))}${APPLIES.map((a) => option(a, a))}</select></span></label
|
|
186
186
|
><label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric" autocomplete="off"
|
|
187
187
|
placeholder="follow the model (64000, 64k or off)" .value=${own.budget}></label
|
|
188
188
|
><div class="ctx-pop-error llmctx-error" ?hidden=${!error}>${error || ""}</div
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<link rel="stylesheet" href="/assets/tokens.css">
|
|
9
9
|
<link rel="stylesheet" href="/assets/annotate_bar.css">
|
|
10
10
|
<link rel="stylesheet" href="/assets/composer.css">
|
|
11
|
+
<link rel="stylesheet" href="/assets/forms.css">
|
|
11
12
|
<style>
|
|
12
13
|
/* The participant page (participant.js): the thread with the composer pinned, and for a review session its subjects
|
|
13
14
|
(the Plan): beside the thread on a wide screen, a Plan | Thread toggle on a narrow one (the composer pinned in both).
|
|
@@ -24,7 +24,7 @@ import { appendQuote } from "./annotations.js";
|
|
|
24
24
|
import { messageAuthorData, participantQuote, participantSource } from "./participant_annotate.js";
|
|
25
25
|
import { mountComposer } from "./composer_mount.js";
|
|
26
26
|
import { PROMPT_LINE_PX } from "./composer_size.js";
|
|
27
|
-
import { forgetReviewHistory, readReviewHistory, recordReviewComment } from "./review_history.js";
|
|
27
|
+
import { forgetReviewHistory, readReviewHistory, recordReviewComment, rememberReviewHistory } from "./review_history.js";
|
|
28
28
|
|
|
29
29
|
// A link's #code= opened in a browser that already has a cookie: traded
|
|
30
30
|
// first, the page reloads without it.
|
|
@@ -48,6 +48,7 @@ let liveStale = false;
|
|
|
48
48
|
let source = null;
|
|
49
49
|
let reloadTimer = null;
|
|
50
50
|
let retryTimer = null;
|
|
51
|
+
let usageTimer = null;
|
|
51
52
|
// Reads of the review that failed in a row (the retry's backoff), and the
|
|
52
53
|
// latest read asked for (an older one answering after it is dropped).
|
|
53
54
|
let failures = 0;
|
|
@@ -93,8 +94,10 @@ function showEnded(json) {
|
|
|
93
94
|
if (ended) return;
|
|
94
95
|
ended = true;
|
|
95
96
|
source?.close();
|
|
97
|
+
clearTimeout(usageTimer);
|
|
98
|
+
usageTimer = null;
|
|
96
99
|
// The link is over: its comments' ↑/↓ list goes with it.
|
|
97
|
-
forgetReviewHistory(data?.viewer?.id);
|
|
100
|
+
forgetReviewHistory(data?.viewer?.id, ownerView ? null : sessionId);
|
|
98
101
|
for (const id of ["head", "main", "dock", "plan"]) $(id).hidden = true;
|
|
99
102
|
$("endedText").textContent = endedText(json);
|
|
100
103
|
$("ended").hidden = false;
|
|
@@ -123,6 +126,7 @@ async function load() {
|
|
|
123
126
|
failures = 0;
|
|
124
127
|
$("offline").hidden = true;
|
|
125
128
|
data = json;
|
|
129
|
+
if (!ownerView) rememberReviewHistory(sessionId, data?.viewer?.id);
|
|
126
130
|
live = liveFrom(data.current_turn);
|
|
127
131
|
liveStale = false;
|
|
128
132
|
render();
|
|
@@ -428,9 +432,11 @@ function renderDock() {
|
|
|
428
432
|
}
|
|
429
433
|
|
|
430
434
|
// The participant's own usage, one line under the comment box (only what
|
|
431
|
-
// their grant's exposure shows; the server sends nothing else).
|
|
432
|
-
//
|
|
435
|
+
// their grant's exposure shows; the server sends nothing else). Spent and
|
|
436
|
+
// queued change on re-reads; the link's time redraws each minute without one.
|
|
433
437
|
function renderUsage(usage) {
|
|
438
|
+
clearTimeout(usageTimer);
|
|
439
|
+
usageTimer = null;
|
|
434
440
|
const items = usageItems(usage);
|
|
435
441
|
const row = $("usage");
|
|
436
442
|
row.hidden = items.length === 0;
|
|
@@ -440,6 +446,9 @@ function renderUsage(usage) {
|
|
|
440
446
|
span.title = item.title;
|
|
441
447
|
return span;
|
|
442
448
|
}));
|
|
449
|
+
if (!ended && Number.isFinite(Date.parse(usage?.expires_at || ""))) {
|
|
450
|
+
usageTimer = setTimeout(() => renderUsage(usage), 60_000);
|
|
451
|
+
}
|
|
443
452
|
}
|
|
444
453
|
|
|
445
454
|
function showError(text) {
|
|
@@ -189,13 +189,13 @@ export function panelTemplate(grants, { now, sessionId, invite = null, error = n
|
|
|
189
189
|
data-act="start-review" ?disabled=${reviewing}>${reviewing ? "Starting…" : "Start a review of it"}</button></div>` : nothing}
|
|
190
190
|
${invite ? inviteTemplate(invite, { canShare }) : html`<form class="pp-invite">
|
|
191
191
|
<div class="pp-invite-row"><input name="name" type="text" placeholder="Name (Alex)" autocomplete="off" aria-label="Their name" maxlength="64"
|
|
192
|
-
><select name="kind" aria-label="A person or an agent"><option value="human">person</option><option value="agent">agent</option></select></div>
|
|
192
|
+
><span class="select-wrap"><select name="kind" aria-label="A person or an agent"><option value="human">person</option><option value="agent">agent</option></select></span></div>
|
|
193
193
|
${identityHeader ? html`<div class="pp-invite-row"><input name="identity" type="email" placeholder="Their email (alex@example.com)"
|
|
194
194
|
autocomplete="off" aria-label="Their email: the link opens only for that account"></div>
|
|
195
195
|
<div class="ctx-pop-meta pp-bind-note">With their email the link opens only for them, signed in through the proxy.
|
|
196
196
|
Without one, anyone the proxy lets in who gets the link can open it.</div>` : nothing}
|
|
197
|
-
<div class="pp-invite-row"><label>Expires <select name="days">${EXPIRY_DAYS.map((d) => html`<option value=${d}
|
|
198
|
-
?selected=${d === DEFAULT_EXPIRY_DAYS}>${d === 1 ? "1 day" : `${d} days`}</option>`)}</select></label
|
|
197
|
+
<div class="pp-invite-row"><label>Expires <span class="select-wrap"><select name="days">${EXPIRY_DAYS.map((d) => html`<option value=${d}
|
|
198
|
+
?selected=${d === DEFAULT_EXPIRY_DAYS}>${d === 1 ? "1 day" : `${d} days`}</option>`)}</select></span></label
|
|
199
199
|
><label>Cap $<input name="cap" type="text" inputmode="decimal" placeholder="2" autocomplete="off" aria-label="Spending cap, dollars"></label
|
|
200
200
|
><button type="submit" class="pp-invite-btn" ?disabled=${busy}>Invite</button></div></form>`}
|
|
201
201
|
<div class="ctx-pop-actions pp-foot">${invite ? html`<button type="button" data-act="another">Invite another</button>` : nothing}<a
|
|
@@ -15,6 +15,20 @@ export function isContinue(pq) {
|
|
|
15
15
|
return !!pq && pq.kind === "continue";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// A model's recommended option: its label ends in "(Recommended)" (the
|
|
19
|
+
// ask_user_question description asks for it first, the mark at the end).
|
|
20
|
+
// The answer keeps the full label; only what the card shows drops the mark.
|
|
21
|
+
const RECOMMENDED = /\s*\(Recommended\)$/;
|
|
22
|
+
export function isRecommended(label) {
|
|
23
|
+
return RECOMMENDED.test(String(label ?? "").trim());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// An option's label as the card shows it: without the recommended mark.
|
|
27
|
+
export function optionText(label) {
|
|
28
|
+
const s = String(label ?? "");
|
|
29
|
+
return isRecommended(s) ? s.trim().replace(RECOMMENDED, "") || s : s;
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
export const CONTINUE_REASON_PLACEHOLDER = "Stop, because… (the model reads it)";
|
|
19
33
|
|
|
20
34
|
// Why a step-limit question closed unanswered (its question_cancelled reason).
|
|
@@ -135,7 +149,7 @@ export function resultText(pq, { answer = null, cancelled = false, reason = "" }
|
|
|
135
149
|
return `Allowed: ${sel[0]}`;
|
|
136
150
|
}
|
|
137
151
|
const parts = [];
|
|
138
|
-
if (sel.length) parts.push(sel.join(", "));
|
|
152
|
+
if (sel.length) parts.push(sel.map(optionText).join(", "));
|
|
139
153
|
if (freeform) parts.push(freeform);
|
|
140
154
|
return parts.length ? `Answered: ${parts.join(" · ")}` : "Answered";
|
|
141
155
|
}
|
|
@@ -16,8 +16,8 @@ import { dismissQuestion, sendAnswer, sendCommand } from "./data.js";
|
|
|
16
16
|
import { diffTemplate } from "./diff_view.js";
|
|
17
17
|
import { render } from "./vendor/lit-html.js";
|
|
18
18
|
import {
|
|
19
|
-
answerErrorText, approvalAllowed, approvalView, CONTINUE_REASON_PLACEHOLDER, deniedReason, isContinue,
|
|
20
|
-
nextCardAction, relayedToView, relayView, resultText, summaryText,
|
|
19
|
+
answerErrorText, approvalAllowed, approvalView, CONTINUE_REASON_PLACEHOLDER, deniedReason, isContinue, isRecommended,
|
|
20
|
+
keepsAskingText, nextCardAction, optionText, relayedToView, relayView, resultText, summaryText,
|
|
21
21
|
} from "./question_card.js";
|
|
22
22
|
import { sessionHash } from "./route.js";
|
|
23
23
|
import { clientLabel, isOwn } from "./turn_events.js";
|
|
@@ -129,6 +129,15 @@ export function createQuestionCards({
|
|
|
129
129
|
text.textContent = String(label);
|
|
130
130
|
row.appendChild(input);
|
|
131
131
|
row.appendChild(text);
|
|
132
|
+
// The model's pick: marked, its mark a tag, nothing pre-checked.
|
|
133
|
+
if (!approval && isRecommended(label)) {
|
|
134
|
+
row.classList.add("recommended");
|
|
135
|
+
text.textContent = optionText(label);
|
|
136
|
+
const tag = doc.createElement("span");
|
|
137
|
+
tag.className = "question-option-tag";
|
|
138
|
+
tag.textContent = "recommended";
|
|
139
|
+
row.appendChild(tag);
|
|
140
|
+
}
|
|
132
141
|
list.appendChild(row);
|
|
133
142
|
});
|
|
134
143
|
card.appendChild(list);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { readStored, removeStored, writeStored } from "./storage.js";
|
|
9
9
|
|
|
10
10
|
export const REVIEW_HISTORY_PREFIX = "chi_review_history:";
|
|
11
|
+
const REVIEW_HISTORY_SESSION_PREFIX = "chi_review_history_session:";
|
|
11
12
|
// How many comments a grant's list keeps (the oldest go first).
|
|
12
13
|
export const REVIEW_HISTORY_MAX = 50;
|
|
13
14
|
|
|
@@ -46,7 +47,14 @@ export function recordReviewComment(grantId, text) {
|
|
|
46
47
|
writeStored(key, JSON.stringify(withComment(readReviewHistory(grantId), text)));
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export function
|
|
50
|
-
|
|
50
|
+
export function rememberReviewHistory(sessionId, grantId) {
|
|
51
|
+
if (sessionId && grantId) writeStored(`${REVIEW_HISTORY_SESSION_PREFIX}${sessionId}`, grantId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function forgetReviewHistory(grantId, sessionId) {
|
|
55
|
+
const sessionKey = sessionId ? `${REVIEW_HISTORY_SESSION_PREFIX}${sessionId}` : null;
|
|
56
|
+
const remembered = sessionKey ? readStored(sessionKey) : null;
|
|
57
|
+
if (sessionKey) removeStored(sessionKey);
|
|
58
|
+
const key = reviewHistoryKey(grantId || remembered);
|
|
51
59
|
if (key) removeStored(key);
|
|
52
60
|
}
|
|
@@ -16,6 +16,9 @@ export function chipModel(summary) {
|
|
|
16
16
|
const label = summary.label || summary.name;
|
|
17
17
|
const parts = [`Toolkit: ${label}, the tools this session declares.`];
|
|
18
18
|
if (summary.fallback) parts.push(`${summary.fallback}: every tool is declared (full) until it resolves.`);
|
|
19
|
+
if (Array.isArray(summary.groups)) {
|
|
20
|
+
parts.push(`Source groups: ${summary.groups.length ? summary.groups.join(", ") : "none"}.`);
|
|
21
|
+
}
|
|
19
22
|
const reachable = Array.isArray(summary.reachable) ? summary.reachable : [];
|
|
20
23
|
const removed = Array.isArray(summary.removed) ? summary.removed : [];
|
|
21
24
|
if (reachable.length) {
|
|
@@ -103,10 +103,19 @@ module Samagotchi
|
|
|
103
103
|
# @return [Result]
|
|
104
104
|
# @raise [Timeout]
|
|
105
105
|
# @raise [SystemCallError] the command can't run (ENOENT, EACCES)
|
|
106
|
-
|
|
106
|
+
# +before_timeout+ lets a test synchronize the deadline with a
|
|
107
|
+
# child-side ready signal; the timeout starts after it returns.
|
|
108
|
+
def call(argv, timeout:, before_timeout: nil)
|
|
107
109
|
Open3.popen3(*argv, pgroup: true) do |stdin, out, err, wait|
|
|
108
110
|
stdin.close
|
|
109
111
|
readers = [out, err].map { |io| Thread.new { io.read }.tap { |t| t.report_on_exception = false } }
|
|
112
|
+
begin
|
|
113
|
+
before_timeout&.call
|
|
114
|
+
rescue StandardError
|
|
115
|
+
stop(wait.pid)
|
|
116
|
+
readers.each { |reader| reader.join(1) }
|
|
117
|
+
raise
|
|
118
|
+
end
|
|
110
119
|
unless wait.join(timeout)
|
|
111
120
|
stop(wait.pid)
|
|
112
121
|
readers.each { |reader| reader.join(1) }
|
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.121.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -770,6 +770,7 @@ files:
|
|
|
770
770
|
- lib/samagotchi/web/public/families_view.js
|
|
771
771
|
- lib/samagotchi/web/public/favicon.svg
|
|
772
772
|
- lib/samagotchi/web/public/format.js
|
|
773
|
+
- lib/samagotchi/web/public/forms.css
|
|
773
774
|
- lib/samagotchi/web/public/hold.js
|
|
774
775
|
- lib/samagotchi/web/public/icons/chi-180.png
|
|
775
776
|
- lib/samagotchi/web/public/icons/chi-192.png
|