samagotchi 0.62.0 → 0.63.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 +33 -1
- data/README.md +4 -1
- data/bin/chi +3 -2
- data/docs/cli.md +4 -1
- data/docs/configuration.md +1 -1
- data/docs/context.md +13 -3
- data/docs/hooks.md +6 -3
- data/docs/memory.md +1 -1
- data/docs/plugins.md +89 -2
- data/docs/releasing.md +1 -1
- data/docs/sub-agent.md +6 -0
- data/docs/testing.md +7 -4
- data/lib/samagotchi/answer_refs.rb +18 -26
- data/lib/samagotchi/bridge.rb +23 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +22 -0
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +406 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +104 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +66 -0
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/context_absorber.rb +25 -6
- data/lib/samagotchi/idle_recap.rb +2 -3
- data/lib/samagotchi/image_store.rb +8 -0
- data/lib/samagotchi/plugin/attached_context.rb +7 -4
- data/lib/samagotchi/recap_setup.rb +3 -3
- data/lib/samagotchi/row_refs.rb +3 -4
- data/lib/samagotchi/session.rb +12 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +6 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +26 -9
- data/lib/samagotchi/terminal_ui.rb +31 -11
- data/lib/samagotchi/tools/execute.rb +22 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -4
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/app.js +47 -91
- data/lib/samagotchi/web/public/card.js +73 -23
- data/lib/samagotchi/web/public/chains.js +6 -4
- data/lib/samagotchi/web/public/command_view.js +39 -35
- data/lib/samagotchi/web/public/copy.js +7 -15
- data/lib/samagotchi/web/public/diff_view.js +20 -23
- data/lib/samagotchi/web/public/format.js +0 -48
- data/lib/samagotchi/web/public/images.js +12 -16
- data/lib/samagotchi/web/public/question_cards.js +3 -2
- data/lib/samagotchi/web/public/read_view.js +10 -9
- data/lib/samagotchi/web/public/scope.js +23 -0
- data/lib/samagotchi/web/public/timing.js +0 -7
- data/lib/samagotchi/web/public/turn_html.js +119 -59
- data/lib/samagotchi/web/public/turn_view.js +73 -116
- data/lib/samagotchi/web/public/vendor/directive.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/unsafe-html.js +8 -0
- data/lib/samagotchi/worker.rb +46 -30
- data/lib/samagotchi/worker_inbound.rb +12 -11
- metadata +8 -1
|
@@ -36,12 +36,12 @@ 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,
|
|
40
|
-
import {
|
|
39
|
+
import { chipName, chipsTemplate, imageFiles, restoredChips, thumbsTemplate, turnRefs, turnText } from "./images.js";
|
|
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
|
-
import { BADGES,
|
|
42
|
+
import { BADGES, elementOf, emptyAnswerTemplate, noteBubbleTemplate, stateBadgeTemplate, steerRowTemplate, userBubbleTemplate } from "./turn_html.js";
|
|
43
43
|
import { createStageView, FOLD_MS } from "./stage_view.js";
|
|
44
|
-
import { CONTINUE_OPENER, isOpener, chainDate, chainNeighbours, continueRefusal, continuedText,
|
|
44
|
+
import { CONTINUE_OPENER, isOpener, chainDate, chainNeighbours, continueRefusal, continuedText, openChildrenTemplate } from "./chains.js";
|
|
45
45
|
import { createTaskStopper } from "./task_stop.js";
|
|
46
46
|
import { appendQuote } from "./annotations.js";
|
|
47
47
|
import { parsePresets } from "./annotate_presets.js";
|
|
@@ -49,12 +49,13 @@ import { createAnnotateBar } from "./annotate_bar.js";
|
|
|
49
49
|
import { copyText, installCopy } from "./copy.js";
|
|
50
50
|
import { ctxBarText, ctxBarTip, lastSpeedText, speedText } from "./ctx.js";
|
|
51
51
|
import { createQuestionCards } from "./question_cards.js";
|
|
52
|
-
import {
|
|
52
|
+
import { cardPlace, drawCard, isCard, isNoticeCard, refreshCardBody, splitSnapshotCards, turnNoticePlace } from "./card.js";
|
|
53
53
|
import { PROMPT_LINE_PX, PROMPT_MIN_PX, gripFloor, promptHeight } from "./composer_size.js";
|
|
54
54
|
import { createCommandList } from "./command_list.js";
|
|
55
55
|
import { createPromptHistory } from "./prompt_history.js";
|
|
56
56
|
import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
57
|
-
import {
|
|
57
|
+
import { scopeDir, scopeTemplate } from "./scope.js";
|
|
58
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
58
59
|
import { easeScrollTo, FOLLOW_MS, shouldFollowScroll, keepFollowing } from "./scroll.js";
|
|
59
60
|
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
60
61
|
import { applySessionEvent, flattenFamilies, inTree, sortedByUpdated } from "./sessions_list.js";
|
|
@@ -94,14 +95,12 @@ const infoTextEl = $("#infoText");
|
|
|
94
95
|
const contextChips = createContextChips({
|
|
95
96
|
el: $("#contextChips"),
|
|
96
97
|
api: { list: listContext, show: getContext, detach: detachContext, add: addContext },
|
|
97
|
-
escapeHtml,
|
|
98
98
|
toast: (text) => showToast(text),
|
|
99
99
|
});
|
|
100
100
|
// The open session's LLM context chip in the info bar, and its popover:
|
|
101
101
|
// /llm-context runs in the session's worker.
|
|
102
102
|
const llmContextControl = createLlmContextControl({
|
|
103
103
|
el: infoTextEl,
|
|
104
|
-
escapeHtml,
|
|
105
104
|
summary: () => (selected ? current.llmContext : null),
|
|
106
105
|
session: () => selected,
|
|
107
106
|
running: () => liveTurn.running,
|
|
@@ -116,7 +115,6 @@ let newChatLlmContext = null;
|
|
|
116
115
|
const newChatLlmSummary = () => (modelPickEl.hidden ? null : modelPicker.chosenRow()?.llm_context || null);
|
|
117
116
|
const llmContextNewControl = createLlmContextControl({
|
|
118
117
|
el: llmContextNewEl,
|
|
119
|
-
escapeHtml,
|
|
120
118
|
summary: newChatLlmSummary,
|
|
121
119
|
kind: NEW_CHAT,
|
|
122
120
|
choice: () => newChatLlmContext,
|
|
@@ -393,29 +391,11 @@ function followSelectedSession(sess) {
|
|
|
393
391
|
// new chat starts on the start page.
|
|
394
392
|
function renderScope() {
|
|
395
393
|
const el = $("#scope");
|
|
396
|
-
|
|
397
|
-
if (scopeError) {
|
|
398
|
-
el.innerHTML = `<span class="scope-name error" title="${escapeHtml(scopeError)}">folder not found</span>${allLink}`;
|
|
399
|
-
} else if (scope.projectName) {
|
|
400
|
-
el.innerHTML = `<span class="scope-name" title="Sessions of ${escapeHtml(scope.projectDir)}">${escapeHtml(scope.projectName)}</span>${allLink}`;
|
|
401
|
-
} else {
|
|
402
|
-
const where = scope.dir || scope.serverDir;
|
|
403
|
-
const backLink = scope.backName
|
|
404
|
-
? `<a class="scope-all scope-back" href="${escapeHtml(projectScopeHref(scope.backDir, location.hash, location.search))}" title="Sessions of ${escapeHtml(scope.backName)} only">← ${escapeHtml(scope.backName)}</a>`
|
|
405
|
-
: "";
|
|
406
|
-
el.innerHTML = `<span class="scope-name" title="Sessions of every project">all projects</span>${backLink}${where ? `<span class="scope-where" title="New chats start in ${escapeHtml(where)}">new chat in<span class="path"><bdi>${escapeHtml(where)}</bdi></span></span>` : ""}`;
|
|
407
|
-
}
|
|
394
|
+
render(scopeTemplate(scope, { error: scopeError, where: () => ({ hash: location.hash, search: location.search }) }), el);
|
|
408
395
|
el.hidden = false;
|
|
409
396
|
// The tab: "<session title> · Chi · <project>" (updateSessionTitle).
|
|
410
397
|
tabBase = scope.projectName ? `Chi · ${scope.projectName}` : "Chi";
|
|
411
398
|
updateSessionTitle();
|
|
412
|
-
// The links follow the hash (an open session, the all-sessions view).
|
|
413
|
-
el.querySelector(".scope-all:not(.scope-back)")?.addEventListener("click", (e) => {
|
|
414
|
-
e.currentTarget.href = allScopeHref(location.hash, scope.dir, location.search);
|
|
415
|
-
});
|
|
416
|
-
el.querySelector(".scope-back")?.addEventListener("click", (e) => {
|
|
417
|
-
e.currentTarget.href = projectScopeHref(scope.backDir, location.hash, location.search);
|
|
418
|
-
});
|
|
419
399
|
}
|
|
420
400
|
|
|
421
401
|
// The session lists (lists_view.js): the strip, all sessions, their cards,
|
|
@@ -585,7 +565,7 @@ function updateComposerMode() {
|
|
|
585
565
|
actionBtn.textContent = "Start";
|
|
586
566
|
actionBtn.disabled = false;
|
|
587
567
|
emptyEl.classList.remove("hidden");
|
|
588
|
-
historyEl.
|
|
568
|
+
historyEl.replaceChildren();
|
|
589
569
|
updateInfoBar();
|
|
590
570
|
} else if (current.owner === "tui") {
|
|
591
571
|
setPlaceholder("A terminal chi owns this session · run it with chi --shared to send from here", "A terminal chi owns this session");
|
|
@@ -699,7 +679,7 @@ async function select(id) {
|
|
|
699
679
|
selected = id;
|
|
700
680
|
setSessionHash(id);
|
|
701
681
|
lists.paintActive();
|
|
702
|
-
historyEl.
|
|
682
|
+
historyEl.replaceChildren(hintEl("Loading…"));
|
|
703
683
|
emptyEl.classList.add("hidden");
|
|
704
684
|
closeStream();
|
|
705
685
|
resetSessionState();
|
|
@@ -741,7 +721,7 @@ async function resync({ endText = null } = {}) {
|
|
|
741
721
|
data = await getSession(id, { parts: true });
|
|
742
722
|
} catch (e) {
|
|
743
723
|
if (e.status === 404) return leaveGoneSession(id, "not_found");
|
|
744
|
-
if (id === selected) historyEl.
|
|
724
|
+
if (id === selected) historyEl.replaceChildren(hintEl(`Error: ${e.message}`));
|
|
745
725
|
return;
|
|
746
726
|
}
|
|
747
727
|
if (id !== selected) return;
|
|
@@ -833,16 +813,30 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
833
813
|
updateComposerMode();
|
|
834
814
|
}
|
|
835
815
|
|
|
816
|
+
// The history's one line while it has nothing else (loading, an error,
|
|
817
|
+
// no output yet).
|
|
818
|
+
function hintEl(text) {
|
|
819
|
+
const el = document.createElement("div");
|
|
820
|
+
el.className = "hint";
|
|
821
|
+
el.textContent = text;
|
|
822
|
+
return el;
|
|
823
|
+
}
|
|
824
|
+
|
|
836
825
|
function renderHistory(items) {
|
|
837
826
|
if (!items.length) {
|
|
838
|
-
historyEl.
|
|
827
|
+
historyEl.replaceChildren(hintEl("No output yet. Send a message."));
|
|
839
828
|
return;
|
|
840
829
|
}
|
|
841
830
|
if (typeof items[0] === "object" && items[0].role) {
|
|
842
|
-
historyEl.replaceChildren(turnHistoryNodes(items, current.timing, { thumbs: (images) =>
|
|
831
|
+
historyEl.replaceChildren(turnHistoryNodes(items, current.timing, { thumbs: (images) => thumbsTemplate(selected, images) }));
|
|
843
832
|
linkTextRefs();
|
|
844
833
|
} else {
|
|
845
|
-
historyEl.
|
|
834
|
+
historyEl.replaceChildren(...items.map((c) => {
|
|
835
|
+
const el = document.createElement("div");
|
|
836
|
+
el.className = "bubble output";
|
|
837
|
+
el.textContent = String(c);
|
|
838
|
+
return el;
|
|
839
|
+
}));
|
|
846
840
|
}
|
|
847
841
|
historyEl.scrollTop = historyEl.scrollHeight;
|
|
848
842
|
}
|
|
@@ -984,7 +978,7 @@ function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false
|
|
|
984
978
|
if (kind !== "gone") liveTurn.timing.ended += 1;
|
|
985
979
|
// A completed turn with no answer: its notice where the answer goes
|
|
986
980
|
// (the turn view places it), else after the turn; before the timing.
|
|
987
|
-
const endNote = emptyAnswer ?
|
|
981
|
+
const endNote = emptyAnswer ? elementOf(emptyAnswerTemplate(emptyAnswer)) : null;
|
|
988
982
|
const kept = turnDom.turnEnded({ kind, endNote, emptyAnswer: !!emptyAnswer }) || [];
|
|
989
983
|
if (endNote && !endNote.isConnected) appendToHistory(endNote);
|
|
990
984
|
liveTurn.timing.finish({ canceled: kind === "canceled" });
|
|
@@ -1006,8 +1000,8 @@ function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false
|
|
|
1006
1000
|
function makeUserBubble(prompt, { state = null, label = null, enqueuedId = null, own = false, images = null } = {}) {
|
|
1007
1001
|
const hint = historyEl.querySelector(".hint");
|
|
1008
1002
|
if (hint) hint.remove();
|
|
1009
|
-
const div =
|
|
1010
|
-
label, state, own, enqueuedId, key: normalize(prompt), thumbs:
|
|
1003
|
+
const div = elementOf(userBubbleTemplate({ content: prompt }, {
|
|
1004
|
+
label, state, own, enqueuedId, key: normalize(prompt), thumbs: thumbsTemplate(selected, images),
|
|
1011
1005
|
}));
|
|
1012
1006
|
const follow = isNearBottom();
|
|
1013
1007
|
appendToHistory(div);
|
|
@@ -1024,7 +1018,7 @@ function setBubbleState(bubble, state) {
|
|
|
1024
1018
|
}
|
|
1025
1019
|
bubble.classList.add(state);
|
|
1026
1020
|
bubble.dataset.userState = state;
|
|
1027
|
-
bubble.
|
|
1021
|
+
bubble.append(elementOf(stateBadgeTemplate(state)));
|
|
1028
1022
|
}
|
|
1029
1023
|
|
|
1030
1024
|
function bubbleById(enqueuedId) {
|
|
@@ -1139,10 +1133,9 @@ function showSteers(steers) {
|
|
|
1139
1133
|
for (const steer of steers || []) {
|
|
1140
1134
|
if (turnDom.steer(steer)) continue;
|
|
1141
1135
|
removeHintIfEmpty();
|
|
1142
|
-
const
|
|
1143
|
-
holder.innerHTML = steerRowHtml(steer, { bubble: true });
|
|
1136
|
+
const row = elementOf(steerRowTemplate(steer, { bubble: true }));
|
|
1144
1137
|
const follow = isNearBottom();
|
|
1145
|
-
appendToHistory(
|
|
1138
|
+
appendToHistory(row);
|
|
1146
1139
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1147
1140
|
}
|
|
1148
1141
|
}
|
|
@@ -1170,36 +1163,6 @@ function cardElement(id) {
|
|
|
1170
1163
|
return findInTurnRoots(".plugin-card").find((el) => el.dataset.cardId === String(id)) || null;
|
|
1171
1164
|
}
|
|
1172
1165
|
|
|
1173
|
-
// A notice is a <details> (a one-line row that opens), a card a <div>: a
|
|
1174
|
-
// card that turns into the other kind (btw's "thinking…" → its answer) is
|
|
1175
|
-
// drawn on a new element in its place. Returns the element drawn.
|
|
1176
|
-
function drawCard(el, card) {
|
|
1177
|
-
const notice = isNoticeCard(card);
|
|
1178
|
-
if (el.tagName !== (notice ? "DETAILS" : "DIV")) {
|
|
1179
|
-
const fresh = document.createElement(notice ? "details" : "div");
|
|
1180
|
-
el.replaceWith(fresh);
|
|
1181
|
-
el = fresh;
|
|
1182
|
-
}
|
|
1183
|
-
el.className = cardClass(card);
|
|
1184
|
-
el.dataset.cardId = card.id;
|
|
1185
|
-
// A running turn's own card (the stage's status waits on one that asks).
|
|
1186
|
-
if (card.in_turn) el.dataset.inTurn = "1";
|
|
1187
|
-
else delete el.dataset.inTurn;
|
|
1188
|
-
if (notice) {
|
|
1189
|
-
const open = el.open;
|
|
1190
|
-
el.innerHTML = noticeInnerHtml(card);
|
|
1191
|
-
el.open = open;
|
|
1192
|
-
} else if (card.body_html === undefined && el.dataset.body === String(card.body ?? "") && el.querySelector(".card-body")) {
|
|
1193
|
-
// The same body again from the stream: keep the rendered one.
|
|
1194
|
-
const body = el.querySelector(".card-body").innerHTML;
|
|
1195
|
-
el.innerHTML = cardInnerHtml({ ...card, body_html: body });
|
|
1196
|
-
} else {
|
|
1197
|
-
el.innerHTML = cardInnerHtml(card);
|
|
1198
|
-
}
|
|
1199
|
-
el.dataset.body = String(card.body ?? "");
|
|
1200
|
-
return el;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
1166
|
// Show +card+: update the one with its id where it is, else add it (a
|
|
1204
1167
|
// running turn's card as a row of its step; else before +before+ or at the
|
|
1205
1168
|
// end of the history). A running turn's card with actions asks the user
|
|
@@ -1326,8 +1289,7 @@ function refreshCardBodies() {
|
|
|
1326
1289
|
if (id !== selected) return;
|
|
1327
1290
|
for (const card of data.cards || []) {
|
|
1328
1291
|
const el = card.type === "card" && cardElement(card.id);
|
|
1329
|
-
|
|
1330
|
-
if (body && typeof card.body_html === "string" && el.dataset.body === String(card.body ?? "")) body.innerHTML = card.body_html;
|
|
1292
|
+
if (el) refreshCardBody(el, card);
|
|
1331
1293
|
}
|
|
1332
1294
|
} catch (_) {}
|
|
1333
1295
|
}, 150);
|
|
@@ -1882,9 +1844,7 @@ function commandBubble(commandId) {
|
|
|
1882
1844
|
|
|
1883
1845
|
function addNoteBubble(note) {
|
|
1884
1846
|
removeHintIfEmpty();
|
|
1885
|
-
const div =
|
|
1886
|
-
div.className = "bubble note";
|
|
1887
|
-
div.innerHTML = noteHtml(note);
|
|
1847
|
+
const div = elementOf(noteBubbleTemplate(note));
|
|
1888
1848
|
const follow = isNearBottom();
|
|
1889
1849
|
appendToHistory(div);
|
|
1890
1850
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
@@ -1964,7 +1924,7 @@ const lightboxEl = $("#lightbox");
|
|
|
1964
1924
|
let chips = [];
|
|
1965
1925
|
|
|
1966
1926
|
function renderChips() {
|
|
1967
|
-
|
|
1927
|
+
render(chipsTemplate(chips), chipsEl);
|
|
1968
1928
|
chipsEl.hidden = !chips.length;
|
|
1969
1929
|
}
|
|
1970
1930
|
|
|
@@ -2222,7 +2182,7 @@ async function continueSelected() {
|
|
|
2222
2182
|
if (selected === id) await select(refusal.id);
|
|
2223
2183
|
showToast(`${short} is continued already: ${selected === refusal.id ? "opened" : "see"} ${refusal.id.slice(0, 8)}`);
|
|
2224
2184
|
} else if (refusal.kind === "children") {
|
|
2225
|
-
showToast(
|
|
2185
|
+
showToast(openChildrenTemplate(id, refusal.children, sessionHash), null, { sticky: true });
|
|
2226
2186
|
} else {
|
|
2227
2187
|
showToast(`Can't continue ${short}: ${refusal.text}`, null, { sticky: true });
|
|
2228
2188
|
}
|
|
@@ -2319,24 +2279,20 @@ function placeToast() {
|
|
|
2319
2279
|
}
|
|
2320
2280
|
}
|
|
2321
2281
|
|
|
2322
|
-
// sticky: stays until its action or its ✕ (else gone after 6 s).
|
|
2282
|
+
// sticky: stays until its action or its ✕ (else gone after 6 s). +text+:
|
|
2283
|
+
// words, or a template (chains.js openChildrenTemplate: a link in it, a
|
|
2284
|
+
// delegate a continue waits for, opens that session and hides the toast).
|
|
2323
2285
|
let toastTimer = null;
|
|
2324
|
-
|
|
2325
|
-
function showToast(text, action = null, { sticky = false, html = false } = {}) {
|
|
2286
|
+
function showToast(text, action = null, { sticky = false } = {}) {
|
|
2326
2287
|
const el = $("#toast");
|
|
2327
|
-
|
|
2328
|
-
|
|
2288
|
+
const hide = () => { el.hidden = true; };
|
|
2289
|
+
render(html`<span @click=${(e) => { if (e.target.closest?.("a[href]")) hide(); }}>${text}</span>${action
|
|
2290
|
+
? html`<button type="button" class="toast-action" @click=${() => { hide(); action.run(); }}>${action.label}</button>`
|
|
2291
|
+
: nothing}${sticky ? html`<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss" @click=${hide}>✕</button>` : nothing}`, el);
|
|
2329
2292
|
placeToast();
|
|
2330
2293
|
el.hidden = false;
|
|
2331
|
-
el.querySelector(".toast-action")?.addEventListener("click", () => {
|
|
2332
|
-
el.hidden = true;
|
|
2333
|
-
action.run();
|
|
2334
|
-
});
|
|
2335
|
-
el.querySelector(".toast-close")?.addEventListener("click", () => { el.hidden = true; });
|
|
2336
|
-
// A link in it (a delegate a continue waits for) opens that session.
|
|
2337
|
-
el.querySelectorAll("a[href]").forEach((a) => a.addEventListener("click", () => { el.hidden = true; }));
|
|
2338
2294
|
clearTimeout(toastTimer);
|
|
2339
|
-
if (!sticky) toastTimer = setTimeout(
|
|
2295
|
+
if (!sticky) toastTimer = setTimeout(hide, 6000);
|
|
2340
2296
|
}
|
|
2341
2297
|
|
|
2342
2298
|
// Annotate (annotate_bar.js): a selection in the conversation offers
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// A plugin's card (docs/plugins.md, Cards): a title, a body and actions,
|
|
2
2
|
// each action a command line the session runs. app.js places and updates
|
|
3
|
-
// the element; this module draws it and says where it
|
|
4
|
-
|
|
3
|
+
// the element; this module draws it (lit-html templates) and says where it
|
|
4
|
+
// goes.
|
|
5
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
6
|
+
import { unsafeHTML } from "./vendor/directives/unsafe-html.js";
|
|
7
|
+
import { recapPlace } from "./format.js";
|
|
5
8
|
|
|
6
9
|
// The card's class: a warn card wears the warning colour; a notice is a
|
|
7
10
|
// one-line row.
|
|
@@ -31,13 +34,13 @@ export function noticeCardLine(card) {
|
|
|
31
34
|
return title && body ? `${title}: ${body}` : title || body;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
// A notice
|
|
35
|
-
//
|
|
36
|
-
export function
|
|
37
|
+
// A notice inside its <details>: the line as the summary, and where it
|
|
38
|
+
// came from when the row is opened (the line wraps then).
|
|
39
|
+
export function noticeTemplate(card) {
|
|
37
40
|
const title = String(card?.title ?? "").trim();
|
|
38
41
|
const source = String(card?.source ?? "").trim();
|
|
39
|
-
const from = source && source !== title ? `<div class="notice-from">from <span class="card-source">${
|
|
40
|
-
return `<summary>${
|
|
42
|
+
const from = source && source !== title ? html`<div class="notice-from">from <span class="card-source">${source}</span></div>` : nothing;
|
|
43
|
+
return html`<summary>${noticeCardLine(card)}</summary>${from}`;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
// Whether a card shown as a row of a step leaves the turn's block when the
|
|
@@ -51,27 +54,74 @@ export function leavesBlock({ warn = false, kind = "completed" } = {}) {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// The body: the server's body_html (rendered markdown, sanitized; or the
|
|
54
|
-
// escaped text in a <pre>), else the plain text in a <pre> (a
|
|
55
|
-
// carries no html; the page's ?cards=1 re-read brings it)
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
// escaped text in a <pre>) as it is, else the plain text in a <pre> (a
|
|
58
|
+
// live event carries no html; the page's ?cards=1 re-read brings it);
|
|
59
|
+
// null for none. The same body_html again keeps its nodes (unsafeHTML
|
|
60
|
+
// renders a string it rendered last time as it is).
|
|
61
|
+
export function cardBodyTemplate(card) {
|
|
62
|
+
if (typeof card?.body_html === "string") return card.body_html ? unsafeHTML(card.body_html) : null;
|
|
58
63
|
const body = String(card?.body ?? "");
|
|
59
|
-
return body.trim() ? `<pre>${
|
|
64
|
+
return body.trim() ? html`<pre>${body}</pre>` : null;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
// The card
|
|
63
|
-
//
|
|
64
|
-
export function
|
|
65
|
-
const source = card.source ? `<span class="card-source">${
|
|
66
|
-
const
|
|
67
|
-
const bodyHtml = cardBodyHtml(card);
|
|
68
|
-
const body = bodyHtml ? `<div class="card-body">${bodyHtml}</div>` : "";
|
|
67
|
+
// The card: its head (title, source), body and one button per action (the
|
|
68
|
+
// command as its tooltip and data), then the line a failed action writes.
|
|
69
|
+
export function cardTemplate(card) {
|
|
70
|
+
const source = card.source ? html`<span class="card-source">${card.source}</span>` : nothing;
|
|
71
|
+
const body = cardBodyTemplate(card);
|
|
69
72
|
const actions = (card.actions || []).filter((a) => a && a.command);
|
|
70
73
|
const buttons = actions.length
|
|
71
|
-
? `<div class="card-actions">${actions.map((a) =>
|
|
72
|
-
`<button type="button" class="card-action ghost" data-command
|
|
73
|
-
:
|
|
74
|
-
return
|
|
74
|
+
? html`<div class="card-actions">${actions.map((a) =>
|
|
75
|
+
html`<button type="button" class="card-action ghost" data-command=${a.command} title=${a.command}>${a.label || a.command}</button>`)}</div>`
|
|
76
|
+
: nothing;
|
|
77
|
+
return html`<div class="card-head"><span class="card-title">${card.title || ""}</span>${source}</div>${body ? html`<div class="card-body">${body}</div>` : nothing}${buttons}<div class="card-error hidden"></div>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A card element → the card it shows, as drawCard drew it.
|
|
81
|
+
const DRAWN = new WeakMap();
|
|
82
|
+
|
|
83
|
+
// +card+ drawn on +el+ (a notice on a <details>, a card on a <div>; one
|
|
84
|
+
// that turns into the other kind, btw's "thinking…" → its answer, is
|
|
85
|
+
// drawn on a new element in its place). Returns the element drawn. A
|
|
86
|
+
// redraw writes only what changed: an open notice stays open, a body
|
|
87
|
+
// that didn't change keeps its nodes (a selection in it), and the same
|
|
88
|
+
// body again from the stream (text, no html) keeps the rendered one. A
|
|
89
|
+
// failed action's line goes, as the card is new.
|
|
90
|
+
export function drawCard(el, card) {
|
|
91
|
+
const notice = isNoticeCard(card);
|
|
92
|
+
if (el.tagName !== (notice ? "DETAILS" : "DIV")) {
|
|
93
|
+
const fresh = el.ownerDocument.createElement(notice ? "details" : "div");
|
|
94
|
+
el.replaceWith(fresh);
|
|
95
|
+
el = fresh;
|
|
96
|
+
}
|
|
97
|
+
el.className = cardClass(card);
|
|
98
|
+
el.dataset.cardId = card.id;
|
|
99
|
+
// A running turn's own card (the stage's status waits on one that asks).
|
|
100
|
+
if (card.in_turn) el.dataset.inTurn = "1";
|
|
101
|
+
else delete el.dataset.inTurn;
|
|
102
|
+
const drawn = DRAWN.get(el);
|
|
103
|
+
const sameBody = card.body_html === undefined && drawn && String(drawn.body ?? "") === String(card.body ?? "");
|
|
104
|
+
paint(el, sameBody ? { ...card, body_html: drawn.body_html } : card);
|
|
105
|
+
const error = el.querySelector(":scope > .card-error");
|
|
106
|
+
if (error) {
|
|
107
|
+
error.textContent = "";
|
|
108
|
+
error.classList.add("hidden");
|
|
109
|
+
}
|
|
110
|
+
return el;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function paint(el, card) {
|
|
114
|
+
render(isNoticeCard(card) ? noticeTemplate(card) : cardTemplate(card), el);
|
|
115
|
+
DRAWN.set(el, card);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The rendered body a re-read brought (the snapshot's +card+ with its
|
|
119
|
+
// body_html) onto a card drawn with the same body text; a no-op for any
|
|
120
|
+
// other.
|
|
121
|
+
export function refreshCardBody(el, card) {
|
|
122
|
+
const drawn = DRAWN.get(el);
|
|
123
|
+
if (!drawn || isNoticeCard(drawn) || typeof card?.body_html !== "string" || String(drawn.body ?? "") !== String(card.body ?? "")) return;
|
|
124
|
+
paint(el, { ...drawn, body_html: card.body_html });
|
|
75
125
|
}
|
|
76
126
|
|
|
77
127
|
// Where a snapshot's card or between-turns notice goes: before the user
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `day N` chip, its earlier links in a popover); the session page links the
|
|
7
7
|
// previous and the next link. Pure: lists_view.js places the chip and rows
|
|
8
8
|
// (lit-html templates) and app.js the toast's markup, and they bind them.
|
|
9
|
-
import {
|
|
9
|
+
import { normalize, previewForCard, relativeTime } from "./format.js";
|
|
10
10
|
import { stoppedBadge, waitingBadge } from "./sessions_list.js";
|
|
11
11
|
import { waitingOn } from "./notify.js";
|
|
12
12
|
import { archivedBadge, rowBadgesTemplate, rowDotTemplate } from "./families_view.js";
|
|
@@ -226,10 +226,12 @@ export function continueRefusal(error, sessions = []) {
|
|
|
226
226
|
// continue can't move to the next link (an older chi's worker, a chi
|
|
227
227
|
// REPL), each a link to its session with what to do about it.
|
|
228
228
|
// @param hrefFor (id) → its page's href
|
|
229
|
-
export function
|
|
229
|
+
export function openChildrenTemplate(prevId, children, hrefFor) {
|
|
230
230
|
const n = children.length;
|
|
231
|
-
const items = children.map((c) => `<li><a href
|
|
232
|
-
|
|
231
|
+
const items = children.map((c) => html`<li><a href=${hrefFor(c.id)} class=${"cr-child"}>${c.short}</a>${c.text ? ` ${c.text}` : nothing}</li>`);
|
|
232
|
+
const line = `Can't continue ${String(prevId).slice(0, 8)}: ${n} open ${n === 1 ? "delegate" : "delegates"} can't move to the next link. ` +
|
|
233
|
+
`Do what ${n === 1 ? "it says" : "each says"}, or wait for ${n === 1 ? "it" : "them"}, then continue.`;
|
|
234
|
+
return html`<div class="continue-refused"><div>${line}</div><ul>${items}</ul></div>`;
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
// The toast after a continue: the new link, the previous one archived (when
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// A tool call's full command in a richer UI than the TUI's params line:
|
|
2
2
|
// the server's view (ToolView: an execute/task_create's command uncut up to
|
|
3
|
-
// 8,000 chars, the call's cwd: argument).
|
|
4
|
-
// row (turn_html.js
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
// 8,000 chars, the call's cwd: argument). lit-html templates for the tool
|
|
4
|
+
// row (turn_html.js toolRowTemplate, live and reloaded alike).
|
|
5
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
6
|
+
import { copyButtonTemplate } from "./copy.js";
|
|
7
7
|
|
|
8
8
|
// What a row's title hovers: the full command when the call has one,
|
|
9
9
|
// else the params line when the row shows a title in its place.
|
|
@@ -15,7 +15,7 @@ export function rowHover(row) {
|
|
|
15
15
|
// The expanded row's command block: "in <cwd>" when the call gave one (as
|
|
16
16
|
// given: execute's may be relative to the project root), the command as
|
|
17
17
|
// written (whitespace kept, its own scroll), a copy button, and a line
|
|
18
|
-
// saying how much a cut command left out.
|
|
18
|
+
// saying how much a cut command left out. nothing for a call without a view.
|
|
19
19
|
//
|
|
20
20
|
// A view with steps (CommandSteps) shows them in the command's place, its
|
|
21
21
|
// leading cd as one more "in" tag, and a "raw" toggle (a checkbox, CSS
|
|
@@ -28,30 +28,33 @@ export function rowHover(row) {
|
|
|
28
28
|
// +title+ is the row's: the model's description of the command, when it
|
|
29
29
|
// gave one, cut to fit. The block starts with the whole description when
|
|
30
30
|
// the title is cut, never in place of the command.
|
|
31
|
-
export function
|
|
31
|
+
export function commandBlockTemplate(view, title = null) {
|
|
32
32
|
const command = view?.command;
|
|
33
|
-
if (!command) return
|
|
33
|
+
if (!command) return nothing;
|
|
34
34
|
const desc = view.description && view.description !== title
|
|
35
|
-
? `<div class="activity-command-desc">${
|
|
36
|
-
:
|
|
35
|
+
? html`<div class="activity-command-desc">${view.description}</div>`
|
|
36
|
+
: nothing;
|
|
37
37
|
const steps = Array.isArray(view.steps) && view.steps.length ? view.steps : null;
|
|
38
38
|
const dirs = [view.cwd, steps && view.cd].filter(Boolean);
|
|
39
39
|
const where = dirs.length
|
|
40
|
-
? `in ${dirs.map((d) =>
|
|
41
|
-
:
|
|
40
|
+
? html`in ${dirs.map((d, i) => html`${i ? html` <span class="activity-command-sep">›</span> ` : nothing}<span>${d}</span>`)}`
|
|
41
|
+
: null;
|
|
42
42
|
const cut = view.truncated
|
|
43
|
-
? `<div class="activity-command-cut">… (${command.length.toLocaleString("en-US")} of ${Number(view.chars || 0).toLocaleString("en-US")} chars)</div>`
|
|
44
|
-
:
|
|
45
|
-
const raw = `<pre><code>${
|
|
43
|
+
? html`<div class="activity-command-cut">… (${command.length.toLocaleString("en-US")} of ${Number(view.chars || 0).toLocaleString("en-US")} chars)</div>`
|
|
44
|
+
: nothing;
|
|
45
|
+
const raw = html`<pre><code>${command}</code></pre>`;
|
|
46
46
|
if (!steps) {
|
|
47
|
-
const cwd = where ? `<div class="activity-command-cwd">${where}</div>` :
|
|
48
|
-
return `<div class="activity-command code-wrap">${desc}${cwd}${raw}${
|
|
47
|
+
const cwd = where ? html`<div class="activity-command-cwd">${where}</div>` : nothing;
|
|
48
|
+
return html`<div class="activity-command code-wrap">${desc}${cwd}${raw}${copyButtonTemplate("code")}${cut}</div>`;
|
|
49
49
|
}
|
|
50
50
|
// No "in" tag: the toggle alone, floated beside the first step (an
|
|
51
|
-
// empty line for it would sit above the steps).
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// empty line for it would sit above the steps). lit never binds the
|
|
52
|
+
// checkbox: a user's toggle stays across a redraw.
|
|
53
|
+
const toggle = html`<label class="activity-command-raw" title="Show the command as written"><input type="checkbox">raw</label>`;
|
|
54
|
+
const head = where
|
|
55
|
+
? html`<div class="activity-command-head"><span class="activity-command-cwd">${where}</span>${toggle}</div>`
|
|
56
|
+
: html`<div class="activity-command-head bare">${toggle}</div>`;
|
|
57
|
+
return html`<div class="activity-command code-wrap has-steps">${desc}${head}${stepsTemplate(steps)}${raw}${copyButtonTemplate("code")}${cut}</div>`;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// What an op between two steps reads as: an arrow for "then" (&&, ;, a
|
|
@@ -59,26 +62,27 @@ export function commandBlockHtml(view, title = null) {
|
|
|
59
62
|
const OP_GLYPHS = { "&&": "→", ";": "→", "\n": "→", "|": "→", "|&": "→", "||": "else", "&": "&" };
|
|
60
63
|
const OP_NAMES = { "\n": "new line", "&": "after starting the step before in the background" };
|
|
61
64
|
|
|
62
|
-
function
|
|
63
|
-
if (!op) return `<span class="step-op"></span>`;
|
|
65
|
+
function opTemplate(op) {
|
|
66
|
+
if (!op) return html`<span class="step-op"></span>`;
|
|
64
67
|
const glyph = OP_GLYPHS[op] || op;
|
|
65
68
|
const name = OP_NAMES[op] || op;
|
|
66
|
-
return `<span class
|
|
69
|
+
return html`<span class=${`step-op${op === "||" ? " else" : ""}`} title=${name}>${glyph}</span>`;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
// A step's redirections to or from nowhere (2>&1, 2>/dev/null, </dev/null),
|
|
70
73
|
// dimmed: plumbing, not what the step does.
|
|
71
74
|
const PLUMBING = /(^|\s)(\d*>&\d+|&>\/dev\/null|\d*>>?\s*\/dev\/null|<\s*\/dev\/null)(?=\s|$)/g;
|
|
72
75
|
|
|
73
|
-
export function
|
|
74
|
-
|
|
76
|
+
export function stepTextTemplate(text) {
|
|
77
|
+
const out = [];
|
|
75
78
|
let at = 0;
|
|
76
79
|
for (const m of String(text).matchAll(PLUMBING)) {
|
|
77
80
|
const start = m.index + m[1].length;
|
|
78
|
-
out
|
|
81
|
+
out.push(text.slice(at, start), html`<span class="step-plumbing">${m[2]}</span>`);
|
|
79
82
|
at = start + m[2].length;
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
out.push(String(text).slice(at));
|
|
85
|
+
return out;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
function heredocLabel({ tag, lines }) {
|
|
@@ -88,25 +92,25 @@ function heredocLabel({ tag, lines }) {
|
|
|
88
92
|
// A step's heredoc chip: the first heredoc's tag and size, and "+N" for
|
|
89
93
|
// the others when the step has more (CommandSteps' heredocs:, as gh api
|
|
90
94
|
// -f body="$(cat <<EOF …)" -f title="$(cat <<EOF …)"), as a title with
|
|
91
|
-
// more steps reads "+N"; the hover lists them all.
|
|
95
|
+
// more steps reads "+N"; the hover lists them all. nothing without one.
|
|
92
96
|
export function heredocChip(step) {
|
|
93
|
-
if (!step?.heredoc) return
|
|
97
|
+
if (!step?.heredoc) return nothing;
|
|
94
98
|
const all = Array.isArray(step.heredocs) && step.heredocs.length > 1 ? step.heredocs : [step.heredoc];
|
|
95
99
|
const more = all.length > 1 ? ` +${all.length - 1}` : "";
|
|
96
100
|
const title = all.length > 1
|
|
97
101
|
? `${all.length} heredocs: ${all.map(heredocLabel).join(", ")}; their text in the raw command`
|
|
98
102
|
: "a heredoc: its text in the raw command";
|
|
99
|
-
return `<span class="step-chip heredoc" title
|
|
103
|
+
return html`<span class="step-chip heredoc" title=${title}>${heredocLabel(all[0])}${more}</span>`;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
// The steps as a list: a label as a small heading above its step, the op
|
|
103
107
|
// that links a step to the one before, its text, its limit and heredoc
|
|
104
108
|
// as chips (beside the text, below it when the row is narrow).
|
|
105
|
-
export function
|
|
109
|
+
export function stepsTemplate(steps) {
|
|
106
110
|
const items = steps.map((step) => {
|
|
107
|
-
const label = step.label ? `<li class="step-label">${
|
|
108
|
-
const limit = step.limit ? `<span class="step-chip" title="output limited">${
|
|
109
|
-
return `${label}<li class="activity-step">${
|
|
111
|
+
const label = step.label ? html`<li class="step-label">${step.label}</li>` : nothing;
|
|
112
|
+
const limit = step.limit ? html`<span class="step-chip" title="output limited">${step.limit}</span>` : nothing;
|
|
113
|
+
return html`${label}<li class="activity-step">${opTemplate(step.op)}<span class="step-body"><code>${stepTextTemplate(step.text)}</code>${limit}${heredocChip(step)}</span></li>`;
|
|
110
114
|
});
|
|
111
|
-
return `<ol class="activity-steps">${items
|
|
115
|
+
return html`<ol class="activity-steps">${items}</ol>`;
|
|
112
116
|
}
|