samagotchi 0.66.0 → 0.67.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 +60 -1
- data/docs/architecture.md +50 -28
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +71 -29
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -1
- data/docs/testing.md +17 -3
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/engine.rb +96 -191
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +21 -11
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +16 -9
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +1 -1
- data/lib/samagotchi/web/public/app.js +31 -36
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/commands.js +2 -2
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/llm_context_chip.js +2 -2
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +14 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_model.js +2 -2
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +11 -8
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +14 -1
|
@@ -36,7 +36,7 @@ import { createShownTexts } from "./shown_texts.js";
|
|
|
36
36
|
import { MODEL_KEY } from "./model_pick.js";
|
|
37
37
|
import { emptySessionState, sessionStateFrom } from "./session_state.js";
|
|
38
38
|
import { createModelPicker } from "./model_picker.js";
|
|
39
|
-
import { chipName, chipsTemplate, imageFiles, restoredChips, thumbsTemplate, turnRefs, turnText } from "./images.js";
|
|
39
|
+
import { chipName, chipsTemplate, imageFiles, restoredChips, thumbsTemplate, turnRefs, turnText, uploadChips } from "./images.js";
|
|
40
40
|
import { normalize, firstPreview, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary, promptNotesChip } from "./format.js";
|
|
41
41
|
import { createTurnView, stepText, turnHistoryNodes } from "./turn_view.js";
|
|
42
42
|
import { BADGES, elementOf, emptyAnswerTemplate, noteBubbleTemplate, stateBadgeTemplate, steerRowTemplate, userBubbleTemplate } from "./turn_html.js";
|
|
@@ -291,13 +291,6 @@ const questionCards = createQuestionCards({
|
|
|
291
291
|
removeHintIfEmpty: () => removeHintIfEmpty(),
|
|
292
292
|
askingText: () => turnDom.askingText(),
|
|
293
293
|
});
|
|
294
|
-
// The event id (or, from an older worker, the event_seq) the selected session
|
|
295
|
-
// was rendered at (null: no live worker then), where a stream opened later
|
|
296
|
-
// has to start.
|
|
297
|
-
let selectSeq = null;
|
|
298
|
-
// The stream was opened from 0 (a wake with no cursor to go on) and no
|
|
299
|
-
// new turn has started on it yet: see followWokenWorker.
|
|
300
|
-
let streamFromGuess = false;
|
|
301
294
|
// The enqueued_ids this page sent (from the /turn acks): only these come back
|
|
302
295
|
// into the composer when their turn fails.
|
|
303
296
|
const sentIds = new Set();
|
|
@@ -383,8 +376,9 @@ function openSessionEvents() {
|
|
|
383
376
|
// never forcing it) or when a newer chi is installed than chi web runs.
|
|
384
377
|
// Once per notice per tab: every reconnect's snapshot says it again.
|
|
385
378
|
const versions = { served: null, installed: null };
|
|
386
|
-
// The
|
|
387
|
-
|
|
379
|
+
// The sessions whose Restart is under way (their button says so): one
|
|
380
|
+
// session's restart never blocks another's.
|
|
381
|
+
const restartingIds = new Set();
|
|
388
382
|
const versionNoticesShown = new Set();
|
|
389
383
|
function noticeVersions(data) {
|
|
390
384
|
if (typeof data.version === "string") versions.served = data.version;
|
|
@@ -767,8 +761,8 @@ async function resync({ endText = null } = {}) {
|
|
|
767
761
|
renderSessionView(data, { kept });
|
|
768
762
|
// Preview is read-only: a worker is woken only by POST /turn, so attach
|
|
769
763
|
// only when a live bridge exists.
|
|
770
|
-
|
|
771
|
-
if (
|
|
764
|
+
current.streamSeq = typeof data.last_event_seq === "number" ? (data.last_event_id || data.last_event_seq) : null;
|
|
765
|
+
if (current.streamSeq !== null) startStream(id, current.streamSeq);
|
|
772
766
|
else if (endText) addEndBubble(endText);
|
|
773
767
|
} finally {
|
|
774
768
|
resyncsInFlight -= 1;
|
|
@@ -1375,8 +1369,7 @@ function removeRecap() {
|
|
|
1375
1369
|
}
|
|
1376
1370
|
|
|
1377
1371
|
// The info bar's "copy chi --attach": the full id, which the page shows
|
|
1378
|
-
// only short; "copied ✓" for 2 s.
|
|
1379
|
-
let attachCopiedAt = 0;
|
|
1372
|
+
// only short; "copied ✓" for 2 s (current.attachCopiedAt: that session's).
|
|
1380
1373
|
infoTextEl.addEventListener("click", (e) => {
|
|
1381
1374
|
if (e.target.closest(".worker-restart")) restartWorker();
|
|
1382
1375
|
// Pushed as openAllView does, so the view's back returns here.
|
|
@@ -1387,8 +1380,9 @@ infoTextEl.addEventListener("click", (e) => {
|
|
|
1387
1380
|
});
|
|
1388
1381
|
infoTextEl.addEventListener("click", async (e) => {
|
|
1389
1382
|
if (!e.target.closest(".copy-attach") || !selected) return;
|
|
1390
|
-
|
|
1391
|
-
|
|
1383
|
+
const id = selected;
|
|
1384
|
+
if (!(await copyText(`chi --attach ${id}`)) || id !== selected) return;
|
|
1385
|
+
current.attachCopiedAt = Date.now();
|
|
1392
1386
|
updateInfoBar();
|
|
1393
1387
|
setTimeout(updateInfoBar, 2000);
|
|
1394
1388
|
});
|
|
@@ -1405,10 +1399,10 @@ function currentWorkerBadge() {
|
|
|
1405
1399
|
// follows it (its bridge_up), the composer's draft stays as it is.
|
|
1406
1400
|
async function restartWorker() {
|
|
1407
1401
|
const badge = currentWorkerBadge();
|
|
1408
|
-
if (!badge || !badge.restart ||
|
|
1402
|
+
if (!badge || !badge.restart || restartingIds.has(selected)) return;
|
|
1409
1403
|
if (!confirm(restartConfirmText(current.workerVersion, badge.newest))) return;
|
|
1410
1404
|
const id = selected;
|
|
1411
|
-
|
|
1405
|
+
restartingIds.add(id);
|
|
1412
1406
|
updateInfoBar();
|
|
1413
1407
|
try {
|
|
1414
1408
|
const result = await restartSession(id);
|
|
@@ -1416,7 +1410,7 @@ async function restartWorker() {
|
|
|
1416
1410
|
} catch (e) {
|
|
1417
1411
|
showToast(`Can't restart: ${String(e.message).replace(/ \(\d+\)$/, "")}`, null, { sticky: true });
|
|
1418
1412
|
} finally {
|
|
1419
|
-
|
|
1413
|
+
restartingIds.delete(id);
|
|
1420
1414
|
updateInfoBar();
|
|
1421
1415
|
}
|
|
1422
1416
|
}
|
|
@@ -1465,7 +1459,7 @@ function updateInfoBar() {
|
|
|
1465
1459
|
// text alone, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
1466
1460
|
renderInfoBar(infoTextEl, {
|
|
1467
1461
|
id: selected,
|
|
1468
|
-
copied: Date.now() - attachCopiedAt < 2000,
|
|
1462
|
+
copied: Date.now() - current.attachCopiedAt < 2000,
|
|
1469
1463
|
status: current.status,
|
|
1470
1464
|
model: modelLabel(current.model, ...(current.served || [])),
|
|
1471
1465
|
// A link of a session chain: ← the previous link, → the next one (each
|
|
@@ -1488,7 +1482,7 @@ function updateInfoBar() {
|
|
|
1488
1482
|
ctx: { text: ctxBarText(current.ctxPct, current.ctxWindowSource), tip: ctxTip },
|
|
1489
1483
|
speed: speedTextNow ? { text: speedTextNow, tip: `Decode speed of the last generation${avgSpeed ? `; session average ${avgSpeed}` : ""}` } : null,
|
|
1490
1484
|
clock: sessionDuration === null ? "" : `session ${formatDuration(sessionDuration)}`,
|
|
1491
|
-
worker: badge ? { badge, busy:
|
|
1485
|
+
worker: badge ? { badge, busy: restartingIds.has(selected) } : null,
|
|
1492
1486
|
});
|
|
1493
1487
|
updateSessionTitle();
|
|
1494
1488
|
}
|
|
@@ -1577,12 +1571,12 @@ function setUsedMemories(names) {
|
|
|
1577
1571
|
// worker have been up already, turn_started says so (replaysDrawnTurn).
|
|
1578
1572
|
function followWokenWorker() {
|
|
1579
1573
|
if (streamControl?.live) return;
|
|
1580
|
-
startStream(selected,
|
|
1581
|
-
streamFromGuess =
|
|
1574
|
+
startStream(selected, current.streamSeq ?? 0);
|
|
1575
|
+
current.streamFromGuess = current.streamSeq === null;
|
|
1582
1576
|
}
|
|
1583
1577
|
|
|
1584
1578
|
function startStream(id, fromSeq) {
|
|
1585
|
-
streamFromGuess = false;
|
|
1579
|
+
current.streamFromGuess = false;
|
|
1586
1580
|
if (streamControl) {
|
|
1587
1581
|
streamControl.close();
|
|
1588
1582
|
streamControl = null;
|
|
@@ -1652,8 +1646,8 @@ const streamHandlers = createStreamHandlers({
|
|
|
1652
1646
|
earlyRestores,
|
|
1653
1647
|
stream: {
|
|
1654
1648
|
takeGuess: () => {
|
|
1655
|
-
const guess = streamFromGuess;
|
|
1656
|
-
streamFromGuess = false;
|
|
1649
|
+
const guess = current.streamFromGuess;
|
|
1650
|
+
current.streamFromGuess = false;
|
|
1657
1651
|
return guess;
|
|
1658
1652
|
},
|
|
1659
1653
|
restart: () => {
|
|
@@ -1806,18 +1800,18 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1806
1800
|
updateInfoBar();
|
|
1807
1801
|
}
|
|
1808
1802
|
actionBtn.disabled = true;
|
|
1803
|
+
// The session the line goes to: the page may switch to another while an
|
|
1804
|
+
// image uploads or the turn is sent, and what follows stays with this one.
|
|
1809
1805
|
const sentTo = selected;
|
|
1810
1806
|
try {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
}
|
|
1814
|
-
const ack = await sendTurn(selected, prompt, { clientId, images: turnRefs(sending), history: typed, delivery });
|
|
1807
|
+
await uploadChips(sending, sentTo, { upload: uploadImage });
|
|
1808
|
+
const ack = await sendTurn(sentTo, prompt, { clientId, images: turnRefs(sending), history: typed, delivery });
|
|
1815
1809
|
if (ack?.command_id) {
|
|
1816
1810
|
// The worker knew it as a command (its list was newer than ours):
|
|
1817
1811
|
// its command_ran shows it, not a prompt bubble.
|
|
1818
1812
|
echo.remove();
|
|
1819
1813
|
clearComposer();
|
|
1820
|
-
followWokenWorker();
|
|
1814
|
+
if (sentTo === selected) followWokenWorker();
|
|
1821
1815
|
return;
|
|
1822
1816
|
}
|
|
1823
1817
|
if (ack?.enqueued_id) sentIds.add(ack.enqueued_id);
|
|
@@ -1834,7 +1828,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1834
1828
|
promptEl.value = composerAfterAck({ sent: line, current: promptEl.value, refill: restored?.refill || "" });
|
|
1835
1829
|
fitPrompt();
|
|
1836
1830
|
clearChips();
|
|
1837
|
-
if (restored?.images?.length) setChips(restoredChips(
|
|
1831
|
+
if (restored?.images?.length) setChips(restoredChips(sentTo, restored.images));
|
|
1838
1832
|
// Its turn_enqueued usually tagged the echo already; the ack covers a
|
|
1839
1833
|
// worker that wasn't live (no turn_enqueued then).
|
|
1840
1834
|
if (ack?.enqueued_id && !echo.dataset.enqueuedId && !bubbleById(ack.enqueued_id)) {
|
|
@@ -1843,17 +1837,18 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1843
1837
|
// A live stream carries this turn already; restarting it would drop the
|
|
1844
1838
|
// events in between. Without one, POST /turn just woke a worker: its
|
|
1845
1839
|
// events start after the seq the session was rendered at (0 for a new
|
|
1846
|
-
// worker process).
|
|
1847
|
-
followWokenWorker();
|
|
1840
|
+
// worker process). A switch since then opened the other session's own.
|
|
1841
|
+
if (sentTo === selected) followWokenWorker();
|
|
1848
1842
|
} catch (e) {
|
|
1849
1843
|
echo.remove();
|
|
1850
1844
|
// The turn never reached the worker's queue: an early restore waiting
|
|
1851
1845
|
// for its ack is never taken back (4.13), so drop it here. The prompt
|
|
1852
1846
|
// is still in the composer, as the echo was removed.
|
|
1853
1847
|
dropEarlyRestores(earlyRestores);
|
|
1854
|
-
// The chips stay (uploaded ones keep their ref) for another try: an
|
|
1848
|
+
// The chips stay (uploaded ones keep their ref, to sentTo) for another try: an
|
|
1855
1849
|
// image refused (bad_image, too_large, bad_images) says why below.
|
|
1856
1850
|
if (e.code === "owned_by_tui") {
|
|
1851
|
+
if (sentTo !== selected) return alert(e.message);
|
|
1857
1852
|
current.owner = "tui";
|
|
1858
1853
|
updateComposerMode();
|
|
1859
1854
|
return;
|
|
@@ -1862,7 +1857,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1862
1857
|
// says so, and the line stays in the composer.
|
|
1863
1858
|
if (e.code === "not_found") return leaveGoneSession(sentTo, "not_found");
|
|
1864
1859
|
// A /queue to a worker that can't queue (it would steer): its words, the text kept.
|
|
1865
|
-
if (e.code === "delivery_unavailable") return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1860
|
+
if (e.code === "delivery_unavailable" && sentTo === selected) return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1866
1861
|
alert(e.message);
|
|
1867
1862
|
} finally {
|
|
1868
1863
|
actionBtn.disabled = current.owner === "tui";
|
|
@@ -141,7 +141,7 @@ export function allViewChains(fams, all, { query = "", includeArchived = false }
|
|
|
141
141
|
|
|
142
142
|
// The links a family's chain list shows: the chain but its head, newest
|
|
143
143
|
// first, each {session, day} (day: its 1-based place in the chain).
|
|
144
|
-
|
|
144
|
+
function chainOthers(f) {
|
|
145
145
|
const links = f?.chain?.links || [];
|
|
146
146
|
return links.map((session, i) => ({ session, day: i + 1 }))
|
|
147
147
|
.filter(({ session }) => session.id !== f.head.id)
|
|
@@ -153,8 +153,8 @@ export function continueLine(choice, reason = "") {
|
|
|
153
153
|
// dropped (styled as failed). A change that restarts the prompt cache asks
|
|
154
154
|
// first (CacheConfirm): `confirm` is the line that applies it, and this
|
|
155
155
|
// tab's own bubble (`own`) gets an Apply button that sends it.
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const QUEUED_TEXT = "queued: runs after this turn";
|
|
157
|
+
const APPLY_TEXT = "Apply (restarts the cache)";
|
|
158
158
|
|
|
159
159
|
export function commandView(event, myId) {
|
|
160
160
|
const own = isOwn(event.client_id, myId);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// its top edge sets a taller floor (the cap then is at least that floor).
|
|
3
3
|
export const PROMPT_MIN_PX = 72; // about three lines of the 15 px field: the start page
|
|
4
4
|
export const PROMPT_LINE_PX = 26; // one line (15 px × 1.45 + 4 padding): in a session
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const PROMPT_CAP_SHARE = 0.35; // of the window's height, for growing with text
|
|
6
|
+
const PROMPT_GRIP_SHARE = 0.5; // of the window's height, for the grip
|
|
7
7
|
|
|
8
8
|
// contentPx: the text's height (scrollHeight plus borders); floorPx: the
|
|
9
9
|
// grip's height, or null; minPx: the empty box's height.
|
|
@@ -99,7 +99,7 @@ function makeButton(doc, kind) {
|
|
|
99
99
|
|
|
100
100
|
// Buttons on every bubble and code block in +root+ (and on +root+ itself)
|
|
101
101
|
// that has none yet.
|
|
102
|
-
|
|
102
|
+
function decorate(root) {
|
|
103
103
|
const doc = root.ownerDocument;
|
|
104
104
|
const bubbles = [...root.querySelectorAll(BUBBLES)];
|
|
105
105
|
if (root.matches(BUBBLES)) bubbles.push(root);
|
|
@@ -13,7 +13,7 @@ function num(v) {
|
|
|
13
13
|
|
|
14
14
|
// What a fill counts against: the window, or the budget when one is set
|
|
15
15
|
// and smaller; null without a window.
|
|
16
|
-
|
|
16
|
+
function countedTokens(windowTokens, budgetTokens = null) {
|
|
17
17
|
const window = num(windowTokens);
|
|
18
18
|
if (!window || window <= 0) return null;
|
|
19
19
|
const budget = num(budgetTokens);
|
|
@@ -21,8 +21,8 @@ import { html, nothing, render } from "./vendor/lit-html.js";
|
|
|
21
21
|
export const QUEUE_LABEL = "queue for after the turn";
|
|
22
22
|
export const CUT_LABEL = "cut in now";
|
|
23
23
|
// A phone's row (≤ 600 px) is short: its text and chips.
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const QUEUE_SHORT = "queue";
|
|
25
|
+
const CUT_SHORT = "cut in";
|
|
26
26
|
// Alt+Enter to a worker without the delivery feature: not sent (a queue
|
|
27
27
|
// would steer it), the text kept, this said.
|
|
28
28
|
export const QUEUE_NEEDS_WORKER = "Not sent: queueing needs a newer worker (it would steer the running turn). "
|
|
@@ -144,7 +144,7 @@ export function deliveryLineTemplate(line, { onCut, onQueue } = {}) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// The idle row: the key hint (none on a phone: no keyboard keys there).
|
|
147
|
-
|
|
147
|
+
function idleTemplate() {
|
|
148
148
|
return html`<span class="delivery-where idle"><span class="long">${IDLE_HINT}</span><span class="short"></span></span>`;
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -155,7 +155,7 @@ export function idleTemplate() {
|
|
|
155
155
|
// phase, gen, tool, cutAfter, serverGen, canQueue, model)
|
|
156
156
|
// composerEmpty() nothing typed and no image chips: a link arms
|
|
157
157
|
// send(delivery) send the composer's line with +delivery+ ("cut", "queue")
|
|
158
|
-
|
|
158
|
+
const TICK_MS = 500;
|
|
159
159
|
|
|
160
160
|
export function createDeliveryControl({ el, read, composerEmpty, send, now = () => Date.now(),
|
|
161
161
|
setTimer = (fn, ms) => setInterval(fn, ms), clearTimer = (t) => clearInterval(t) }) {
|
|
@@ -71,7 +71,7 @@ export function familyRowsTemplate(f, { activeId = null, matchIds = null, archiv
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// The widest window index.html lays out as a phone (@media (max-width:600px)).
|
|
74
|
-
|
|
74
|
+
const PHONE_MAX_PX = 600;
|
|
75
75
|
|
|
76
76
|
// The strip's family popover: one #familyPop in body (position: fixed, so
|
|
77
77
|
// the strip's sideways scroll on a phone doesn't clip it), under the chip
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { waitingOn } from "./notify.js";
|
|
2
2
|
|
|
3
3
|
export const PREVIEW_CHARS = 40;
|
|
4
|
-
|
|
4
|
+
const FIRST_PREVIEW_CHARS = 80;
|
|
5
5
|
|
|
6
6
|
export function escapeHtml(value) {
|
|
7
7
|
return String(value).replace(
|
|
@@ -37,20 +37,24 @@ export function normalize(s) {
|
|
|
37
37
|
return String(s || "").replace(/\s+/g, " ").trim();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// +text+ in at most +n+ characters: whole when it fits, else cut with "…"
|
|
41
|
+
// as its last (the kept part's trailing spaces dropped).
|
|
42
|
+
export function clip(text, n) {
|
|
43
|
+
const s = String(text ?? "");
|
|
44
|
+
return s.length > n ? `${s.slice(0, n - 1).trimEnd()}\u2026` : s;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
export function previewOf(lastPrompt) {
|
|
41
48
|
const normalized = normalize(lastPrompt);
|
|
42
49
|
if (!normalized) return "\u2014";
|
|
43
|
-
|
|
44
|
-
return `${normalized.slice(0, PREVIEW_CHARS)}\u2026`;
|
|
45
|
-
}
|
|
46
|
-
return normalized;
|
|
50
|
+
return clip(normalized, PREVIEW_CHARS + 1);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// A session's first message as the info bar's title: whitespace collapsed,
|
|
50
54
|
// cut at FIRST_PREVIEW_CHARS with an ellipsis ("" for none).
|
|
51
55
|
export function firstPreview(text) {
|
|
52
56
|
const n = normalize(text);
|
|
53
|
-
return n
|
|
57
|
+
return clip(n, FIRST_PREVIEW_CHARS + 1);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export function previewForCard(session) {
|
|
@@ -66,7 +70,7 @@ const DELETE_PREVIEW_CHARS = 40;
|
|
|
66
70
|
// "stopped if it's still running", never promised.
|
|
67
71
|
export function deleteConfirmText(session) {
|
|
68
72
|
const n = normalize(session.first_preview || session.last_prompt || "");
|
|
69
|
-
const preview = n
|
|
73
|
+
const preview = clip(n, DELETE_PREVIEW_CHARS + 1);
|
|
70
74
|
const shortId = session.short_id || String(session.id).slice(0, 8);
|
|
71
75
|
const lines = [
|
|
72
76
|
`Delete session ${shortId}${preview ? ` \u2014 "${preview}"` : ""}?`,
|
|
@@ -67,7 +67,30 @@ export function turnRefs(chips) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Chips again for a prompt the worker handed back (its images are stored
|
|
70
|
-
// already, so they need no upload).
|
|
70
|
+
// already, so they need no upload: to that session).
|
|
71
71
|
export function restoredChips(sessionId, images) {
|
|
72
|
-
return (images || []).filter((i) => i && i.file)
|
|
72
|
+
return (images || []).filter((i) => i && i.file)
|
|
73
|
+
.map((ref) => ({ name: ref.name || "image", ref, session: sessionId, src: imageUrl(sessionId, ref) }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A restored chip has no file of its own: its image, read back from where
|
|
77
|
+
// its session stores it.
|
|
78
|
+
async function fetchChipFile(src, name) {
|
|
79
|
+
const res = await fetch(src);
|
|
80
|
+
if (!res.ok) throw new Error(`Can't read ${name} again (${res.status})`);
|
|
81
|
+
const blob = await res.blob();
|
|
82
|
+
return new File([blob], name, { type: blob.type });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Uploads the chips a turn to +sessionId+ needs: a ref is into one session's
|
|
86
|
+
// image store (chip.session), so a chip uploaded to another one (a failed
|
|
87
|
+
// send there, then a switch) goes up again, to this one. Sets each chip's
|
|
88
|
+
// ref and session in place: a failed send keeps them for another try.
|
|
89
|
+
export async function uploadChips(chips, sessionId, { upload, fetchFile = fetchChipFile }) {
|
|
90
|
+
for (const chip of chips || []) {
|
|
91
|
+
if (chip.ref && chip.session === sessionId) continue;
|
|
92
|
+
const file = chip.file || await fetchFile(chip.src, chip.name);
|
|
93
|
+
chip.ref = await upload(sessionId, file, chip.name);
|
|
94
|
+
chip.session = sessionId;
|
|
95
|
+
}
|
|
73
96
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
const STRATEGIES = [
|
|
18
18
|
{ value: "none", label: "none" },
|
|
19
19
|
{ value: "stale", label: "stale" },
|
|
20
20
|
{ value: "stale,forget", label: "stale + forget (experimental)" },
|
|
@@ -144,7 +144,7 @@ export function choiceWords(choice) {
|
|
|
144
144
|
|
|
145
145
|
// The strategy list, with the session's own value when it isn't one of them
|
|
146
146
|
// (forget alone, a newer chi's layer).
|
|
147
|
-
|
|
147
|
+
function strategyOptions(chosen) {
|
|
148
148
|
if (chosen === DEFAULT || STRATEGIES.some((s) => s.value === chosen)) return STRATEGIES;
|
|
149
149
|
return [...STRATEGIES, { value: chosen, label: chosen }];
|
|
150
150
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// summaries (GET /api/events). The page shows an OS notification and a
|
|
3
3
|
// title badge for what it returns, while the tab is not in front.
|
|
4
4
|
|
|
5
|
+
import { clip } from "./format.js";
|
|
6
|
+
|
|
5
7
|
// The bell's state. OS notifications need a secure context: on chi web
|
|
6
8
|
// over http at a LAN address the API is missing or always denied, so the
|
|
7
9
|
// bell is hidden there (the title badge still counts).
|
|
@@ -16,7 +18,7 @@ export function notifyState({ hasNotification, secureContext, permission, wanted
|
|
|
16
18
|
// chi ran for a delegate child's report (last_turn origin delegate_report)
|
|
17
19
|
// or an attached context change (context): nobody started it, so nobody
|
|
18
20
|
// watched it.
|
|
19
|
-
|
|
21
|
+
const DONE_MIN_SECONDS = 10;
|
|
20
22
|
const UNWATCHED_ORIGINS = ["delegate_report", "context"];
|
|
21
23
|
|
|
22
24
|
// The step-limit question (kind "continue": a turn ran out of iterations
|
|
@@ -90,7 +92,7 @@ const cardKey = (sessionId, cardId) => `${sessionId}:card:${cardId}`;
|
|
|
90
92
|
// @param prev the summary held before (null for a session not seen yet)
|
|
91
93
|
// @param next the new summary (null when the session is gone)
|
|
92
94
|
// @return [String] keys, maybe none
|
|
93
|
-
|
|
95
|
+
function closedKeys(prev, next) {
|
|
94
96
|
const keys = [];
|
|
95
97
|
const q = prev?.pending_question;
|
|
96
98
|
const nq = next?.pending_question;
|
|
@@ -147,7 +149,7 @@ export function trackAttention(state, type, data = {}) {
|
|
|
147
149
|
// No question or answer text (it would show on a lock screen).
|
|
148
150
|
export function attentionText(session, attention) {
|
|
149
151
|
const name = String(session?.title || session?.first_preview || session?.last_prompt || "chi session").trim();
|
|
150
|
-
const title = name
|
|
152
|
+
const title = clip(name, 61);
|
|
151
153
|
const seconds = Math.round(Number(session?.last_turn?.seconds) || 0);
|
|
152
154
|
const body = {
|
|
153
155
|
question: "needs an answer",
|
|
@@ -171,7 +173,7 @@ export function attentionText(session, attention) {
|
|
|
171
173
|
// lowest claim (a tab id) shows it (4.11). Without BroadcastChannel every
|
|
172
174
|
// tab goes on alone, at once.
|
|
173
175
|
export const NOTIFY_CHANNEL = "chi-notify";
|
|
174
|
-
|
|
176
|
+
const NOTIFY_HOLD_MS = 300;
|
|
175
177
|
// How long a key seen in front is remembered (its frame may come late).
|
|
176
178
|
const SEEN_TTL_MS = 10 * 60 * 1000;
|
|
177
179
|
|
|
@@ -70,7 +70,7 @@ export function approvalView(pq) {
|
|
|
70
70
|
const reason = a.reason ? String(a.reason) : "(no reason given)";
|
|
71
71
|
return {
|
|
72
72
|
// A plugin tool's label ("chrome: screenshot"), as its row shows it;
|
|
73
|
-
// else the tool the call acts as (an MCP tool behind
|
|
73
|
+
// else the tool the call acts as (an MCP tool behind call_tool).
|
|
74
74
|
tool: a.label || a.acts_as || a.tool || "",
|
|
75
75
|
// A tool without a command or path (an MCP tool): its compact args.
|
|
76
76
|
what: command || paths.join("\n") || a.args || "",
|
|
@@ -30,7 +30,7 @@ const SKIP = "pre, a, [data-ref-kind]";
|
|
|
30
30
|
// The ref a match names: { path, line, endLine } or null. A path has a `/`
|
|
31
31
|
// or an extension (`10:30` is a time, `host:8080` a port); line 0 is never
|
|
32
32
|
// a ref; an end line not after the line is dropped.
|
|
33
|
-
|
|
33
|
+
function refOf(groups) {
|
|
34
34
|
const path = groups?.path || "";
|
|
35
35
|
if (!path.includes("/") && !/\.[A-Za-z0-9]+$/.test(path)) return null;
|
|
36
36
|
if (groups.from === undefined) return { path, line: null, endLine: null };
|
|
@@ -50,6 +50,15 @@ export function emptySessionState() {
|
|
|
50
50
|
reasoningHistory: null,
|
|
51
51
|
toolkit: null,
|
|
52
52
|
timing: normalizeTiming(),
|
|
53
|
+
// The event id (or, from an older worker, the event_seq) the session was
|
|
54
|
+
// rendered at (null: no live worker then), where a stream opened later
|
|
55
|
+
// has to start; set by the page's resync, not by the payload.
|
|
56
|
+
streamSeq: null,
|
|
57
|
+
// The stream was opened from 0 (a wake with no cursor to go on) and no
|
|
58
|
+
// new turn has started on it yet (app.js followWokenWorker).
|
|
59
|
+
streamFromGuess: false,
|
|
60
|
+
// When the info bar's "copy chi --attach" copied (its "copied ✓").
|
|
61
|
+
attachCopiedAt: 0,
|
|
53
62
|
};
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -65,6 +74,11 @@ export function sessionStateFrom(data) {
|
|
|
65
74
|
served: s.served_model ? [s.served_model, s.served_model_for, s.served_expected_by || null] : null,
|
|
66
75
|
dir: s.working_directory,
|
|
67
76
|
owner: s.owner || null,
|
|
77
|
+
// Its live worker's chi and features (null and [] without one): a
|
|
78
|
+
// switch shows its stale-worker badge, task stop and queue at once,
|
|
79
|
+
// not at the session's next hub event (followSelectedSession).
|
|
80
|
+
workerVersion: s.worker_version || null,
|
|
81
|
+
workerFeatures: Array.isArray(s.worker_features) ? s.worker_features : [],
|
|
68
82
|
timing: normalizeTiming(data.timing),
|
|
69
83
|
};
|
|
70
84
|
// The saved fill, so a reloaded or stopped session shows it before its
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalize } from "./format.js";
|
|
1
|
+
import { clip, normalize } from "./format.js";
|
|
2
2
|
|
|
3
3
|
// Where the open session's title shows: once on screen. Its strip card says
|
|
4
4
|
// it when the strip is shown and has an `.active` card for it; otherwise
|
|
@@ -21,6 +21,5 @@ export function topBarTitle({ title, cardShown, stripHidden, allView }) {
|
|
|
21
21
|
export function tabTitle({ title, base }) {
|
|
22
22
|
const text = normalize(title);
|
|
23
23
|
if (!text) return base;
|
|
24
|
-
|
|
25
|
-
return `${cut} · ${base}`;
|
|
24
|
+
return `${clip(text, TAB_TITLE_CHARS + 1)} · ${base}`;
|
|
26
25
|
}
|
|
@@ -188,14 +188,6 @@ export function familySummary(f) {
|
|
|
188
188
|
return delegatesSummary(f.members.map((m) => m.session));
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// The chip's words: "▸ 3 delegates · 1 live · 1 waiting".
|
|
192
|
-
export function familyChipText(sum) {
|
|
193
|
-
if (!sum) return "";
|
|
194
|
-
const waiting = sum.counts?.waiting || 0;
|
|
195
|
-
return [`\u25B8 ${sum.total} ${sum.total === 1 ? "delegate" : "delegates"}`,
|
|
196
|
-
sum.live ? `${sum.live} live` : "", waiting ? `${waiting} waiting` : ""].filter(Boolean).join(" \u00b7 ");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
191
|
// Whether a family's list is open: the user's toggle (+toggles+, head id →
|
|
200
192
|
// bool) wins; else open when a member waits on the user (the head's own
|
|
201
193
|
// wait is on its card already) or a search matched a member.
|
|
@@ -15,9 +15,9 @@ const FILE_TOOLS = new Set(["read", "edit", "write"]);
|
|
|
15
15
|
const TASK_TOOLS = new Set(["task_wait", "task_get", "task_stop"]);
|
|
16
16
|
const TRAIL = 3;
|
|
17
17
|
// The hand-off waits this long after the stage was last in use.
|
|
18
|
-
|
|
18
|
+
const QUIET_MS = 1500;
|
|
19
19
|
// A touch, wheel or scroll counts as use this long (phones have no hover).
|
|
20
|
-
|
|
20
|
+
const TOUCH_MS = 4000;
|
|
21
21
|
|
|
22
22
|
// One headline sentence as plain text: the markdown a streaming answer
|
|
23
23
|
// carries (headings, quotes, bullets, table pipes, emphasis, code ticks,
|
|
@@ -3,9 +3,9 @@ import { waitingOn } from "./notify.js";
|
|
|
3
3
|
// How many session cards the top strip shows for its width in px: a card
|
|
4
4
|
// needs about 360 px plus a 12 px gap next to the 150 px "All sessions" tile;
|
|
5
5
|
// never fewer than 3 (narrow screens scroll them), never more than 6.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const STRIP_TILE_PX = 150;
|
|
7
|
+
const STRIP_GAP_PX = 12;
|
|
8
|
+
const STRIP_CARD_PX = 360;
|
|
9
9
|
|
|
10
10
|
export function stripColumns(width) {
|
|
11
11
|
const fit = Math.floor(((width || 0) - STRIP_TILE_PX - STRIP_GAP_PX) / (STRIP_CARD_PX + STRIP_GAP_PX));
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// button disabled. Pure but for the injected calls, so `node --test`
|
|
7
7
|
// covers it; turn_view.js and stage_view.js draw the button.
|
|
8
8
|
import { stopTask as postStopTask } from "./data.js";
|
|
9
|
+
import { clip } from "./format.js";
|
|
9
10
|
|
|
10
11
|
// What a worker that has the route names in its sidecar features.
|
|
11
|
-
|
|
12
|
+
const TASK_STOP_FEATURE = "task_stop";
|
|
12
13
|
const COMMAND_SHOWN = 120;
|
|
13
14
|
|
|
14
15
|
// The task a row's button would stop: a running task_wait's (its server
|
|
@@ -61,7 +62,7 @@ export function taskCommand(gens, taskId) {
|
|
|
61
62
|
|
|
62
63
|
export function confirmText(taskId, command) {
|
|
63
64
|
const line = String(command || "").replace(/\s+/g, " ").trim();
|
|
64
|
-
const what =
|
|
65
|
+
const what = line ? clip(line, COMMAND_SHOWN) : taskId;
|
|
65
66
|
return `Stop task ${what}?`;
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -26,7 +26,7 @@ export function workerGoneText({ turnRunning, stopped = false }) {
|
|
|
26
26
|
|
|
27
27
|
// How long a turn's answer waits for the after_turn hooks' answer_display
|
|
28
28
|
// before the page stops waiting and pops what it has (4.14).
|
|
29
|
-
|
|
29
|
+
const DISPLAY_WAIT_MS = 20000;
|
|
30
30
|
|
|
31
31
|
// The wait between a turn_completed with display_pending and its
|
|
32
32
|
// answer_display. The worker can die in between (a crash, a stop, an idle
|
|
@@ -8,7 +8,7 @@ import { html, nothing, render } from "./vendor/lit-html.js";
|
|
|
8
8
|
import { commandBlockTemplate, rowHover } from "./command_view.js";
|
|
9
9
|
import { copySource } from "./copy.js";
|
|
10
10
|
import { activityDiffTemplate } from "./diff_view.js";
|
|
11
|
-
import { steerSender } from "./format.js";
|
|
11
|
+
import { clip, steerSender } from "./format.js";
|
|
12
12
|
import { rowTitleRef } from "./refs.js";
|
|
13
13
|
import { readLinesTemplate } from "./read_view.js";
|
|
14
14
|
import { STOPPED_LINE_CLASS, cancelLineText, formatDuration } from "./timing.js";
|
|
@@ -105,7 +105,7 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
|
|
|
105
105
|
const duration = formatDuration(row.duration_ms);
|
|
106
106
|
const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
107
107
|
const output = full
|
|
108
|
-
? html`<div class="activity-output" title=${full}>${full
|
|
108
|
+
? html`<div class="activity-output" title=${full}>${clip(full, 301)}</div>`
|
|
109
109
|
: nothing;
|
|
110
110
|
const status = statusParts(row.status, { noMatch: !!row.no_match });
|
|
111
111
|
return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// - the turn's end closes the current gen.
|
|
26
26
|
import { addCompleted, addStarted, newActivity, toolName } from "./activity.js";
|
|
27
27
|
import { tallyText } from "./tally.js";
|
|
28
|
-
import { leadTrimmed } from "./format.js";
|
|
28
|
+
import { clip, leadTrimmed } from "./format.js";
|
|
29
29
|
|
|
30
30
|
const LABEL_MAX = 80;
|
|
31
31
|
const LABEL_TOOLS = 3;
|
|
@@ -194,7 +194,7 @@ function isStep(gen) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function cut(line) {
|
|
197
|
-
return line
|
|
197
|
+
return clip(line, LABEL_MAX + 1);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// "3 tool calls (1 failed)": failed = status error, the tally's predicate
|
|
@@ -58,7 +58,7 @@ function closedLabel(gen) {
|
|
|
58
58
|
// that would repeat "thinking"). +body+ is the text: a string, or the live
|
|
59
59
|
// step's Text node (appended by delta: a selection in it survives).
|
|
60
60
|
// +cls+ "bubble thinking" is a plain answer's collapsed thinking block.
|
|
61
|
-
|
|
61
|
+
function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking", on = {} }) {
|
|
62
62
|
const inner = html`<div class="thinking-body">${body}</div>`;
|
|
63
63
|
if (!wrapped) return inner;
|
|
64
64
|
const summary = line === null ? "thinking" : html`<span class="thinking-tag">thinking · </span>${line}`;
|
|
@@ -241,10 +241,6 @@ module Samagotchi
|
|
|
241
241
|
knows_dir(InfoProbe.call(host, port, timeout: timeout))
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
-
def self.probe_verdict(status, body)
|
|
245
|
-
knows_dir(InfoProbe.verdict(status, body))
|
|
246
|
-
end
|
|
247
|
-
|
|
248
244
|
def self.knows_dir(info)
|
|
249
245
|
info.is_a?(Hash) && !Array(info["features"]).include?("dir") ? :other : info
|
|
250
246
|
end
|