samagotchi 0.61.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 +91 -1
- data/README.md +4 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +3 -0
- data/docs/cli.md +26 -9
- data/docs/configuration.md +33 -20
- data/docs/context.md +21 -3
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +14 -3
- data/docs/internals/engine.md +24 -11
- data/docs/internals/prompt-caching.md +6 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +131 -5
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/sub-agent.md +6 -0
- data/docs/testing.md +10 -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/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -6
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -1
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +58 -3
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -4
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- 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/engine.rb +9 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/idle_recap.rb +2 -3
- data/lib/samagotchi/image_store.rb +8 -0
- data/lib/samagotchi/kernel_loop.rb +7 -16
- data/lib/samagotchi/llm/chat_loop.rb +23 -15
- data/lib/samagotchi/plugin/api.rb +36 -18
- data/lib/samagotchi/plugin/attached_context.rb +7 -4
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/recap_setup.rb +3 -3
- data/lib/samagotchi/row_refs.rb +125 -0
- data/lib/samagotchi/session.rb +12 -0
- data/lib/samagotchi/session_metrics.rb +24 -2
- 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/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +57 -14
- data/lib/samagotchi/tool_activity.rb +8 -4
- data/lib/samagotchi/tool_response.rb +5 -4
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +16 -3
- data/lib/samagotchi/tools/builtins.rb +9 -2
- data/lib/samagotchi/tools/execute.rb +37 -7
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/registry.rb +5 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +20 -5
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +6 -3
- data/lib/samagotchi/web/public/app.js +106 -106
- data/lib/samagotchi/web/public/card.js +73 -23
- data/lib/samagotchi/web/public/chains.js +24 -24
- data/lib/samagotchi/web/public/command_view.js +39 -35
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/copy.js +7 -15
- data/lib/samagotchi/web/public/diff_view.js +20 -23
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/format.js +0 -48
- data/lib/samagotchi/web/public/images.js +12 -16
- data/lib/samagotchi/web/public/index.html +61 -10
- data/lib/samagotchi/web/public/keep_focus.js +24 -0
- data/lib/samagotchi/web/public/lists_view.js +13 -13
- data/lib/samagotchi/web/public/llm_context_chip.js +55 -34
- data/lib/samagotchi/web/public/question_card.js +11 -0
- data/lib/samagotchi/web/public/question_cards.js +63 -7
- data/lib/samagotchi/web/public/read_view.js +10 -9
- data/lib/samagotchi/web/public/ref_menu.js +18 -5
- data/lib/samagotchi/web/public/refs.js +113 -15
- data/lib/samagotchi/web/public/scope.js +23 -0
- data/lib/samagotchi/web/public/scroll.js +42 -0
- data/lib/samagotchi/web/public/stage_model.js +22 -3
- data/lib/samagotchi/web/public/stage_view.js +299 -70
- data/lib/samagotchi/web/public/timing.js +1 -8
- data/lib/samagotchi/web/public/turn_html.js +122 -61
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +167 -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 +49 -32
- data/lib/samagotchi/worker_inbound.rb +17 -14
- metadata +10 -1
|
@@ -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
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
// through `continues`, forward through the session whose `continues` names
|
|
5
5
|
// this one. The lists fold a chain into its latest listed link's card (a
|
|
6
6
|
// `day N` chip, its earlier links in a popover); the session page links the
|
|
7
|
-
// previous and the next link. Pure:
|
|
8
|
-
|
|
7
|
+
// previous and the next link. Pure: lists_view.js places the chip and rows
|
|
8
|
+
// (lit-html templates) and app.js the toast's markup, and they bind them.
|
|
9
|
+
import { normalize, previewForCard, relativeTime } from "./format.js";
|
|
9
10
|
import { stoppedBadge, waitingBadge } from "./sessions_list.js";
|
|
10
11
|
import { waitingOn } from "./notify.js";
|
|
12
|
+
import { archivedBadge, rowBadgesTemplate, rowDotTemplate } from "./families_view.js";
|
|
13
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
11
14
|
|
|
12
15
|
// The opener a new link's composer is filled with (never sent: the user
|
|
13
16
|
// edits or sends it). The same for every chain.
|
|
@@ -148,10 +151,11 @@ export function chainOthers(f) {
|
|
|
148
151
|
// The chip in a chain's card: "day 3" (the head's place; "day 2 of 3" when
|
|
149
152
|
// later links aren't listed), with a live dot / "waiting" when one of the
|
|
150
153
|
// other links has a worker / waits on the user (typing in an archived link
|
|
151
|
-
// brings it back, but it stays folded here).
|
|
154
|
+
// brings it back, but it stays folded here). nothing for a family in no
|
|
155
|
+
// chain. A lit-html template, as families_view.js's chip.
|
|
152
156
|
// @param open whether the list is open (aria-expanded)
|
|
153
|
-
export function
|
|
154
|
-
if (!f?.chain) return
|
|
157
|
+
export function chainChipTemplate(f, { open = false } = {}) {
|
|
158
|
+
if (!f?.chain) return nothing;
|
|
155
159
|
const { links, at } = f.chain;
|
|
156
160
|
const others = chainOthers(f);
|
|
157
161
|
const live = others.filter(({ session: s }) => s.owner === "worker").length;
|
|
@@ -162,9 +166,10 @@ export function chainChipHtml(f, { open = false } = {}) {
|
|
|
162
166
|
const later = links.length - 1 - at;
|
|
163
167
|
const parts = [earlier ? `${earlier} earlier ${earlier === 1 ? "link" : "links"}` : "", later ? `${later} later` : ""].filter(Boolean);
|
|
164
168
|
const title = `Session chain: day ${day} of ${links.length} (${parts.join(", ")}); open the list`;
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
return `<button type="button" class
|
|
169
|
+
const liveT = live ? html`<span class="cc-live"> · <span class="live-dot" aria-hidden="true"></span>${live} live</span>` : nothing;
|
|
170
|
+
const waitingT = waiting ? html` · <span class="cc-waiting">${waiting} waiting</span>` : nothing;
|
|
171
|
+
return html`<button type="button" class=${`chain-chip${waiting ? " waiting" : ""}`} aria-expanded=${open ? "true" : "false"}
|
|
172
|
+
title=${title}><span class="cc-arrow" aria-hidden="true">↩</span>${text}${liveT}${waitingT}</button>`;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
// The chain's list (the chip's popover): one row per other link, newest
|
|
@@ -172,7 +177,7 @@ export function chainChipHtml(f, { open = false } = {}) {
|
|
|
172
177
|
// its preview), the waiting / stopped badge, archived. A row's click opens
|
|
173
178
|
// that link (families_view.js's popover binds .family-row[data-id]).
|
|
174
179
|
// @param activeId the open session's id (its row is .active)
|
|
175
|
-
export function
|
|
180
|
+
export function chainRowsTemplate(f, { activeId = null, now = Date.now() } = {}) {
|
|
176
181
|
const byId = new Map((f?.chain?.links || []).map((s) => [s.id, s]));
|
|
177
182
|
const rows = chainOthers(f).map(({ session: s, day }) => {
|
|
178
183
|
const cls = ["family-row", "chain-row"];
|
|
@@ -180,22 +185,15 @@ export function chainRowsHtml(f, { activeId = null, now = Date.now() } = {}) {
|
|
|
180
185
|
if (s.archived) cls.push("archived");
|
|
181
186
|
const waiting = waitingBadge(s, { inFamily: true });
|
|
182
187
|
const stopped = waiting ? null : stoppedBadge(s);
|
|
183
|
-
const dotWord = waiting ? waiting.title : s.status;
|
|
184
188
|
const date = chainDate(s.created_at);
|
|
185
189
|
const ago = relativeTime(s.updated_at || s.created_at || "", now);
|
|
186
|
-
const parts = [
|
|
187
|
-
`<span class="status dot ${escapeHtml(s.status || "")}${waiting ? " waiting" : ""}" title="${escapeHtml(dotWord || "")}" aria-label="${escapeHtml(dotWord || "")}"></span>`,
|
|
188
|
-
`<span class="cr-day">day ${day}</span>`,
|
|
189
|
-
date ? `<span class="cr-date">${escapeHtml(date)}</span>` : "",
|
|
190
|
-
`<span class="fr-preview">${escapeHtml(s.recap || chainTitle(s, byId))}</span>`,
|
|
191
|
-
waiting ? `<span class="attn attn-${waiting.kind}" title="${escapeHtml(waiting.title)}">${escapeHtml(waiting.text)}</span>` : "",
|
|
192
|
-
stopped ? `<span class="stopped-badge" title="${escapeHtml(stopped.title)}">${escapeHtml(stopped.text)}</span>` : "",
|
|
193
|
-
s.archived ? `<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>` : "",
|
|
194
|
-
].join("");
|
|
195
190
|
const tip = `${s.id} · ${s.status || ""}${ago ? ` · updated ${ago}` : ""}`;
|
|
196
|
-
return `<div class
|
|
191
|
+
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${"--depth:1"} role=${"listitem"}><button type="button" class="fr-open"
|
|
192
|
+
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-day">day ${day}</span>${
|
|
193
|
+
date ? html`<span class="cr-date">${date}</span>` : nothing}<span class="fr-preview">${s.recap || chainTitle(s, byId)}</span>${
|
|
194
|
+
rowBadgesTemplate(waiting, stopped)}${s.archived ? archivedBadge : nothing}</button></div>`;
|
|
197
195
|
});
|
|
198
|
-
return `<div class="family-rows chain-rows" role="list">${rows
|
|
196
|
+
return html`<div class="family-rows chain-rows" role="list">${rows}</div>`;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
// What the page does with a refused continue (POST /api/sessions
|
|
@@ -228,10 +226,12 @@ export function continueRefusal(error, sessions = []) {
|
|
|
228
226
|
// continue can't move to the next link (an older chi's worker, a chi
|
|
229
227
|
// REPL), each a link to its session with what to do about it.
|
|
230
228
|
// @param hrefFor (id) → its page's href
|
|
231
|
-
export function
|
|
229
|
+
export function openChildrenTemplate(prevId, children, hrefFor) {
|
|
232
230
|
const n = children.length;
|
|
233
|
-
const items = children.map((c) => `<li><a href
|
|
234
|
-
|
|
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>`;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
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
|
}
|