samagotchi 0.60.0 → 0.62.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 +99 -1
- data/docs/architecture.md +10 -4
- data/docs/cli.md +22 -8
- data/docs/configuration.md +32 -19
- data/docs/context.md +8 -0
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +80 -1
- data/docs/internals/engine.md +57 -15
- data/docs/internals/prompt-caching.md +8 -1
- data/docs/plugins.md +49 -6
- data/docs/sessions.md +7 -3
- data/docs/testing.md +4 -1
- data/lib/samagotchi/answer_display.rb +4 -3
- data/lib/samagotchi/answer_refs.rb +210 -0
- data/lib/samagotchi/bridge/card_store.rb +6 -1
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +27 -3
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
- 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 +1 -1
- data/lib/samagotchi/engine.rb +47 -9
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +3 -1
- data/lib/samagotchi/interrupted_turn.rb +39 -0
- data/lib/samagotchi/kernel_loop.rb +13 -20
- data/lib/samagotchi/llm/chat_loop.rb +28 -17
- data/lib/samagotchi/log_subscriber.rb +6 -3
- data/lib/samagotchi/plugin/api.rb +36 -18
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/ref.rb +25 -8
- data/lib/samagotchi/reply_wait.rb +6 -2
- data/lib/samagotchi/row_refs.rb +126 -0
- data/lib/samagotchi/session.rb +40 -4
- data/lib/samagotchi/session_manager.rb +64 -6
- data/lib/samagotchi/session_metrics.rb +24 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +30 -3
- 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 +15 -3
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/registry.rb +5 -3
- data/lib/samagotchi/turn_checkpoint.rb +128 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +19 -1
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +4 -1
- data/lib/samagotchi/web/public/answer_refs.js +217 -0
- data/lib/samagotchi/web/public/app.js +74 -33
- data/lib/samagotchi/web/public/chains.js +18 -20
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/index.html +66 -13
- 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 +60 -5
- data/lib/samagotchi/web/public/ref_menu.js +24 -3
- data/lib/samagotchi/web/public/ref_scan.js +5 -3
- data/lib/samagotchi/web/public/refs.js +140 -20
- 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/stream_handlers.js +5 -3
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +5 -4
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +121 -17
- data/lib/samagotchi/worker.rb +169 -21
- data/lib/samagotchi/worker_inbound.rb +5 -3
- data/lib/samagotchi/worker_signals.rb +81 -0
- metadata +8 -1
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// provider can attach one: GET …/context's can_add_url) opens a form that
|
|
8
8
|
// attaches a URL (a GitHub PR); the web never adds a command.
|
|
9
9
|
|
|
10
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
11
|
+
import { keepFocus } from "./keep_focus.js";
|
|
12
|
+
|
|
10
13
|
// "now", "3m", "2h", "4d" since an ISO time; "" for none.
|
|
11
14
|
export function ageText(iso, now = Date.now()) {
|
|
12
15
|
const at = Date.parse(iso || "");
|
|
@@ -38,33 +41,40 @@ export function chipModel(rows, now = Date.now()) {
|
|
|
38
41
|
});
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const ADD_TITLE = "Attach a URL (a GitHub PR) the agent can read and hears about when it changes";
|
|
45
|
+
|
|
46
|
+
// The chips (a lit-html template; attributes after a bound one are bound
|
|
47
|
+
// too, so they keep their order). addUrl: end with the "+ URL" chip;
|
|
48
|
+
// open: the chip whose popover is open (a source's name), addOpen: the
|
|
49
|
+
// "+ URL" form is.
|
|
50
|
+
export function chipsTemplate(chips, { addUrl = false, open = null, addOpen = false } = {}) {
|
|
51
|
+
return html`${chips.map((chip) => html`<button type="button" class=${`ctx-chip ${chip.state}${chip.name === open ? " open" : ""}`}
|
|
52
|
+
data-name=${chip.name} title=${chip.title} aria-haspopup=${"dialog"}>${
|
|
53
|
+
chip.state === "unread" ? html`<span class="ctx-dot" aria-hidden="true"></span>` : nothing}${chip.label}</button>`)}${
|
|
54
|
+
addUrl ? html`<button type="button" class=${`ctx-chip ctx-add${addOpen ? " open" : ""}`} data-act=${"add"} title=${ADD_TITLE}
|
|
55
|
+
aria-haspopup=${"dialog"}>+ URL</button>` : nothing}`;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
// The "+ URL" popover: the URL, an optional why, Attach; +
|
|
50
|
-
//
|
|
51
|
-
export function
|
|
52
|
-
return `<form class="ctx-add-form"><div class="ctx-pop-head"><strong>Attach a URL</strong
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// The "+ URL" popover: the URL, an optional why, Attach; +error+ after a
|
|
59
|
+
// try that failed, +busy+ while one runs.
|
|
60
|
+
export function addFormTemplate({ error = null, busy = false } = {}) {
|
|
61
|
+
return html`<form class="ctx-add-form"><div class="ctx-pop-head"><strong>Attach a URL</strong
|
|
62
|
+
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
63
|
+
><div class="ctx-pop-meta">A link an installed bundle knows (github-pr: a pull request). The agent gets a note when it changes.</div
|
|
64
|
+
><input class="ctx-add-input" name="url" type="url" inputmode="url" placeholder="https://github.com/owner/repo/pull/123" required autocomplete="off"
|
|
65
|
+
><input class="ctx-add-input" name="why" type="text" placeholder="why (optional)" maxlength="300" autocomplete="off"
|
|
66
|
+
>${error ? html`<div class="ctx-pop-error" role="alert">${error}</div>` : nothing
|
|
67
|
+
}<div class="ctx-pop-actions"><button type="submit" class="ctx-add-submit" ?disabled=${busy}>Attach</button></div></form>`;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
// The popover's inside for one row (GET …/context's)
|
|
62
|
-
|
|
70
|
+
// The popover's inside for one row (GET …/context's); +text+ the row's
|
|
71
|
+
// text once "view text" read it (null before).
|
|
72
|
+
export function popoverTemplate(row, { text = null, now = Date.now() } = {}) {
|
|
63
73
|
const kind = row.kind === "cmd" ? `runs every ${row.every_seconds ? `${row.every_seconds}s` : "few minutes"}` : "pushed";
|
|
64
74
|
const url = hintUrl(row.hint);
|
|
65
75
|
const hint = url
|
|
66
|
-
? `<a class="ctx-pop-hint" href
|
|
67
|
-
: (row.hint ? `<div class="ctx-pop-hint">${
|
|
76
|
+
? html`<a class="ctx-pop-hint" href=${url} target=${"_blank"} rel=${"noopener noreferrer"}>${url}</a>`
|
|
77
|
+
: (row.hint ? html`<div class="ctx-pop-hint">${row.hint}</div>` : nothing);
|
|
68
78
|
const age = ageText(row.fetched_at, now);
|
|
69
79
|
const meta = [age ? (age === "now" ? "fetched just now" : `fetched ${age} ago`) : "no text yet",
|
|
70
80
|
row.unread ? "the agent hasn't read this change" : (row.has_text ? "the agent has read it" : "")]
|
|
@@ -73,29 +83,32 @@ export function popoverHtml(row, escapeHtml, now = Date.now()) {
|
|
|
73
83
|
const detachTitle = row.scope === "project"
|
|
74
84
|
? "The project's source: this session stops seeing it (chi context unmute brings it back)"
|
|
75
85
|
: "Remove it from this session";
|
|
76
|
-
return `<div class="ctx-pop-head"><strong>${
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`<div class="ctx-pop-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(row.has_text ? `<button type="button" data-act="view">view text</button>` : "") +
|
|
87
|
-
`<button type="button" class="ctx-pop-detach" data-act="detach" title="${escapeHtml(detachTitle)}">${detach}</button></div>`;
|
|
86
|
+
return html`<div class="ctx-pop-head"><strong>${row.name}</strong
|
|
87
|
+
><span class="ctx-pop-kind">${row.scope} · ${kind}</span
|
|
88
|
+
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
89
|
+
>${row.why ? html`<div class="ctx-pop-why">${row.why}</div>` : nothing}${hint}${
|
|
90
|
+
row.summary ? html`<div class="ctx-pop-summary">${row.summary}</div>` : nothing
|
|
91
|
+
}<div class="ctx-pop-meta">${meta}</div
|
|
92
|
+
>${row.error ? html`<div class="ctx-pop-error">last refresh failed: ${row.error}</div>` : nothing
|
|
93
|
+
}<pre class="ctx-pop-text" ?hidden=${text === null}>${text ?? ""}</pre
|
|
94
|
+
><div class="ctx-pop-actions">${row.has_text && text === null ? html`<button type="button" data-act="view">view text</button>` : nothing
|
|
95
|
+
}<button type="button" class="ctx-pop-detach" data-act="detach" title=${detachTitle}>${detach}</button></div>`;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
// The chips for the session bar. el: #contextChips; api: {list(id),
|
|
91
|
-
// show(id, name), detach(id, name), add(id, {url, why})};
|
|
92
|
-
//
|
|
93
|
-
|
|
99
|
+
// show(id, name), detach(id, name), add(id, {url, why})}; toast(text).
|
|
100
|
+
// Chips, popover and form are lit templates: a redraw (a refresh, the
|
|
101
|
+
// minute's tick) keeps their nodes, so focus and typed text stay.
|
|
102
|
+
export function createContextChips({ el, api, toast = () => {}, doc = document, win = window }) {
|
|
94
103
|
let sessionId = null;
|
|
95
104
|
let rows = [];
|
|
96
105
|
let canAdd = false;
|
|
97
106
|
let openName = null;
|
|
98
107
|
let loadSeq = 0;
|
|
108
|
+
// The open row's text once read (view text), and the "+ URL" form's
|
|
109
|
+
// state ({error, busy}).
|
|
110
|
+
let text = null;
|
|
111
|
+
let addState = {};
|
|
99
112
|
const pop = doc.createElement("div");
|
|
100
113
|
pop.id = "contextPopover";
|
|
101
114
|
pop.className = "ctx-popover";
|
|
@@ -106,17 +119,20 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
106
119
|
// openName ADD: the "+ URL" form is open.
|
|
107
120
|
const ADD = "\u0000add";
|
|
108
121
|
|
|
109
|
-
function
|
|
122
|
+
function drawChips() {
|
|
110
123
|
const chips = chipModel(rows);
|
|
111
|
-
|
|
124
|
+
// A source inserted before the focused chip: the focus stays on its source.
|
|
125
|
+
keepFocus(el, "data-name", () => render(chipsTemplate(chips, { addUrl: canAdd, open: openName === ADD ? null : openName,
|
|
126
|
+
addOpen: openName === ADD }), el));
|
|
112
127
|
el.hidden = chips.length === 0 && !canAdd;
|
|
113
128
|
// The "+ URL" chip alone sits inline, not on a row of its own.
|
|
114
129
|
el.classList.toggle("only-add", chips.length === 0 && canAdd);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The chips, and the open popover closed when what it is for has gone.
|
|
133
|
+
function draw() {
|
|
134
|
+
if (openName === ADD ? !canAdd : openName && !chipModel(rows).some((chip) => chip.name === openName)) close();
|
|
135
|
+
else drawChips();
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
async function refresh() {
|
|
@@ -134,10 +150,11 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
134
150
|
rows = [];
|
|
135
151
|
canAdd = false;
|
|
136
152
|
}
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
draw();
|
|
154
|
+
// An open popover follows its row, but not while it shows the text.
|
|
155
|
+
if (openName && openName !== ADD && text === null) {
|
|
139
156
|
const row = rows.find((r) => r.name === openName);
|
|
140
|
-
if (row
|
|
157
|
+
if (row) fill(row);
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
|
|
@@ -146,7 +163,6 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
146
163
|
rows = [];
|
|
147
164
|
canAdd = false;
|
|
148
165
|
close();
|
|
149
|
-
render();
|
|
150
166
|
return refresh();
|
|
151
167
|
}
|
|
152
168
|
|
|
@@ -155,11 +171,10 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
155
171
|
rows = [];
|
|
156
172
|
canAdd = false;
|
|
157
173
|
close();
|
|
158
|
-
render();
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
function fill(row) {
|
|
162
|
-
|
|
177
|
+
render(popoverTemplate(row, { text }), pop);
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
// Above the chip, inside the window with a 16px gutter.
|
|
@@ -174,19 +189,30 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
174
189
|
function open(name, chip) {
|
|
175
190
|
const row = rows.find((r) => r.name === name);
|
|
176
191
|
if (!row) return;
|
|
192
|
+
// Another popover's nodes go, so this one starts fresh.
|
|
193
|
+
render(nothing, pop);
|
|
177
194
|
openName = name;
|
|
195
|
+
text = null;
|
|
178
196
|
fill(row);
|
|
179
197
|
pop.hidden = false;
|
|
180
198
|
place(chip);
|
|
181
|
-
|
|
199
|
+
drawChips();
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
|
|
202
|
+
const drawAdd = (changes = {}) => {
|
|
203
|
+
addState = { ...addState, ...changes };
|
|
204
|
+
render(addFormTemplate(addState), pop);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
function openAdd(chip) {
|
|
208
|
+
render(nothing, pop);
|
|
185
209
|
openName = ADD;
|
|
186
|
-
|
|
210
|
+
text = null;
|
|
211
|
+
addState = {};
|
|
212
|
+
drawAdd();
|
|
187
213
|
pop.hidden = false;
|
|
188
214
|
place(chip);
|
|
189
|
-
|
|
215
|
+
drawChips();
|
|
190
216
|
pop.querySelector('input[name="url"]')?.focus();
|
|
191
217
|
}
|
|
192
218
|
|
|
@@ -195,8 +221,7 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
195
221
|
const url = form.elements.url.value.trim();
|
|
196
222
|
const why = form.elements.why.value.trim();
|
|
197
223
|
if (!id || !url) return;
|
|
198
|
-
|
|
199
|
-
if (button) button.disabled = true;
|
|
224
|
+
drawAdd({ busy: true });
|
|
200
225
|
try {
|
|
201
226
|
const data = await api.add(id, { url, why: why || undefined });
|
|
202
227
|
if (id !== sessionId) return;
|
|
@@ -205,32 +230,35 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
205
230
|
toast(`Attached ${data.name}: the agent gets a note before your next message`);
|
|
206
231
|
} catch (e) {
|
|
207
232
|
if (id !== sessionId || openName !== ADD) return;
|
|
208
|
-
|
|
209
|
-
|
|
233
|
+
// The same form again: what the user typed stays; the URL gets focus.
|
|
234
|
+
drawAdd({ busy: false, error: String(e.message).replace(/ \(\d+\)$/, "") });
|
|
235
|
+
pop.querySelector('input[name="url"]')?.focus();
|
|
210
236
|
}
|
|
211
237
|
}
|
|
212
238
|
|
|
213
239
|
function close() {
|
|
214
240
|
openName = null;
|
|
241
|
+
text = null;
|
|
242
|
+
addState = {};
|
|
215
243
|
pop.hidden = true;
|
|
216
|
-
|
|
244
|
+
render(nothing, pop);
|
|
245
|
+
drawChips();
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
async function viewText() {
|
|
220
249
|
const name = openName;
|
|
221
|
-
|
|
222
|
-
if (!name || !pre) return;
|
|
250
|
+
if (!name || name === ADD) return;
|
|
223
251
|
try {
|
|
224
252
|
const data = await api.show(sessionId, name);
|
|
225
253
|
if (name !== openName) return;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
const row = rows.find((r) => r.name === name);
|
|
255
|
+
if (!row) return;
|
|
256
|
+
text = data.text || "";
|
|
257
|
+
fill(row);
|
|
229
258
|
} catch (e) {
|
|
230
259
|
toast(`Can't read ${name}: ${String(e.message).replace(/ \(\d+\)$/, "")}`);
|
|
231
260
|
}
|
|
232
261
|
}
|
|
233
|
-
|
|
234
262
|
async function detach() {
|
|
235
263
|
const row = rows.find((r) => r.name === openName);
|
|
236
264
|
if (!row) return;
|
|
@@ -279,7 +307,7 @@ export function createContextChips({ el, api, escapeHtml, toast = () => {}, doc
|
|
|
279
307
|
});
|
|
280
308
|
win.addEventListener("focus", () => refresh());
|
|
281
309
|
// The ages move on by themselves.
|
|
282
|
-
win.setInterval(() => { if (rows.length)
|
|
310
|
+
win.setInterval(() => { if (rows.length) drawChips(); }, 60_000);
|
|
283
311
|
|
|
284
312
|
return { load, clear, refresh, close, get rows() { return rows; } };
|
|
285
313
|
}
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
// A parent's card with its delegates folded in (docs/sessions.md, the web):
|
|
2
|
-
// the chip that opens the family's list, and the list's rows
|
|
3
|
-
//
|
|
4
|
-
// strip) and binds the clicks.
|
|
5
|
-
|
|
2
|
+
// the chip that opens the family's list, and the list's rows, as lit-html
|
|
3
|
+
// templates; lists_view.js places them (inline in all sessions, a popover
|
|
4
|
+
// over the strip) and binds the clicks. Attributes after a bound one are
|
|
5
|
+
// bound too, so they keep their order in the markup (lit sets bound ones
|
|
6
|
+
// after the static ones).
|
|
7
|
+
import { ownerBadge, previewForCard, relativeTime } from "./format.js";
|
|
6
8
|
import { familySummary, stoppedBadge, waitingBadge } from "./sessions_list.js";
|
|
9
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
10
|
+
import { keepFocus } from "./keep_focus.js";
|
|
7
11
|
|
|
8
12
|
// The chip in a parent's meta row: "▸ 3 delegates · ● 1 live · 1 waiting"
|
|
9
13
|
// (a waiting one in the attention colour), its tooltip the delegates by
|
|
10
|
-
// state.
|
|
14
|
+
// state. nothing for a family with no members.
|
|
11
15
|
// @param open whether the list is open (aria-expanded; the arrow turns)
|
|
12
|
-
export function
|
|
16
|
+
export function familyChipTemplate(f, { open = false } = {}) {
|
|
13
17
|
const sum = familySummary(f);
|
|
14
|
-
if (!sum) return
|
|
18
|
+
if (!sum) return nothing;
|
|
15
19
|
const waiting = sum.counts.waiting;
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
return `<button type="button" class
|
|
20
|
+
const live = sum.live ? html`<span class="fc-live"> · <span class="live-dot" aria-hidden="true"></span>${sum.live} live</span>` : nothing;
|
|
21
|
+
const waitingT = waiting ? html` · <span class="fc-waiting">${waiting} waiting</span>` : nothing;
|
|
22
|
+
return html`<button type="button" class=${`family-chip${waiting ? " waiting" : ""}`} aria-expanded=${open ? "true" : "false"}
|
|
23
|
+
title=${sum.title}><span class="fc-arrow" aria-hidden="true">▸</span>${sum.total} ${sum.total === 1 ? "delegate" : "delegates"}${
|
|
24
|
+
live}${waitingT}</button>`;
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
// A row's status dot (a family's or a chain's): the waiting title over the
|
|
28
|
+
// status when the session waits on the user.
|
|
29
|
+
export function rowDotTemplate(s, waiting) {
|
|
30
|
+
const word = (waiting ? waiting.title : s.status) || "";
|
|
31
|
+
return html`<span class=${`status dot ${s.status || ""}${waiting ? " waiting" : ""}`} title=${word} aria-label=${word}></span>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A row's waiting or stopped badge, and the archived one.
|
|
35
|
+
export function rowBadgesTemplate(waiting, stopped) {
|
|
36
|
+
return html`${waiting ? html`<span class=${`attn attn-${waiting.kind}`} title=${waiting.title}>${waiting.text}</span>` : nothing}${
|
|
37
|
+
stopped ? html`<span class="stopped-badge" title=${stopped.title}>${stopped.text}</span>` : nothing}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const archivedBadge = html`<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>`;
|
|
41
|
+
|
|
21
42
|
// The family's list: one row per member, depth first, indented one step
|
|
22
43
|
// per depth (--depth): status dot, preview, time, the waiting / stopped
|
|
23
44
|
// badge, the owner badge, archived. A row's click opens that session.
|
|
@@ -25,7 +46,7 @@ export function familyChipHtml(f, { open = false } = {}) {
|
|
|
25
46
|
// @param matchIds a search's matching members (.match; the rest .dim), or
|
|
26
47
|
// null without a search
|
|
27
48
|
// @param archivable an archive button per row (all sessions, not in select mode)
|
|
28
|
-
export function
|
|
49
|
+
export function familyRowsTemplate(f, { activeId = null, matchIds = null, archivable = false, now = Date.now() } = {}) {
|
|
29
50
|
const rows = f.members.map(({ session: s, depth }) => {
|
|
30
51
|
const cls = ["family-row"];
|
|
31
52
|
if (activeId && s.id === activeId) cls.push("active");
|
|
@@ -36,23 +57,17 @@ export function familyRowsHtml(f, { activeId = null, matchIds = null, archivable
|
|
|
36
57
|
const stopped = waiting ? null : stoppedBadge(s);
|
|
37
58
|
const owner = ownerBadge(s.owner, s.id);
|
|
38
59
|
const ago = relativeTime(ts, now);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
`<
|
|
42
|
-
`<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
ago ? `<span class="time">${
|
|
46
|
-
owner ? `<span class
|
|
47
|
-
s.archived ?
|
|
48
|
-
].join("");
|
|
49
|
-
const short = escapeHtml(s.short_id || String(s.id).slice(0, 8));
|
|
50
|
-
const arch = !archivable ? "" : s.archived
|
|
51
|
-
? `<button type="button" class="card-arch" data-arch="${escapeHtml(s.id)}" title="Unarchive: back to the lists" aria-label="Unarchive session ${short}"></button>`
|
|
52
|
-
: `<button type="button" class="card-arch" data-arch="${escapeHtml(s.id)}" title="Archive: hide from the lists, keep for good" aria-label="Archive session ${short}"></button>`;
|
|
53
|
-
return `<div class="${cls.join(" ")}" data-id="${escapeHtml(s.id)}" style="--depth:${depth}" role="listitem"><button type="button" class="fr-open" title="${escapeHtml(`${s.id} · ${s.status || ""}`)}">${parts}</button>${arch}</div>`;
|
|
60
|
+
const short = s.short_id || String(s.id).slice(0, 8);
|
|
61
|
+
const arch = !archivable ? nothing : s.archived
|
|
62
|
+
? html`<button type="button" class="card-arch" data-arch=${s.id} title=${"Unarchive: back to the lists"} aria-label=${`Unarchive session ${short}`}></button>`
|
|
63
|
+
: html`<button type="button" class="card-arch" data-arch=${s.id} title=${"Archive: hide from the lists, keep for good"} aria-label=${`Archive session ${short}`}></button>`;
|
|
64
|
+
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${`--depth:${depth}`} role=${"listitem"}><button type="button" class="fr-open"
|
|
65
|
+
title=${`${s.id} · ${s.status || ""}`}>${rowDotTemplate(s, waiting)}<span class="fr-preview">${previewForCard(s)}</span>${
|
|
66
|
+
rowBadgesTemplate(waiting, stopped)}${ago ? html`<span class="time">${ago}</span>` : nothing}${
|
|
67
|
+
owner ? html`<span class=${`owner ${owner.kind}`} title=${owner.title}>${owner.text}</span>` : nothing}${
|
|
68
|
+
s.archived ? archivedBadge : nothing}</button>${arch}</div>`;
|
|
54
69
|
});
|
|
55
|
-
return `<div class="family-rows" role="list">${rows
|
|
70
|
+
return html`<div class="family-rows" role="list">${rows}</div>`;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
// The widest window index.html lays out as a phone (@media (max-width:600px)).
|
|
@@ -61,11 +76,13 @@ export const PHONE_MAX_PX = 600;
|
|
|
61
76
|
// The strip's family popover: one #familyPop in body (position: fixed, so
|
|
62
77
|
// the strip's sideways scroll on a phone doesn't clip it), under the chip
|
|
63
78
|
// it was opened from and as wide as that card (300 px at least, inside the
|
|
64
|
-
// window; on a phone, the window less 16 px gutters). The strip draws again
|
|
65
|
-
// the new chip for the same head,
|
|
79
|
+
// window; on a phone, the window less 16 px gutters). The strip draws again
|
|
80
|
+
// on every hub event: refresh() then finds the new chip for the same head,
|
|
81
|
+
// draws the rows again (lit writes only what changed: a row keeps its
|
|
82
|
+
// nodes, reused by position, so keepFocus keeps the focus on its session)
|
|
66
83
|
// and closes when the family is gone.
|
|
67
|
-
// @param rowsFor (headId) → the rows'
|
|
68
|
-
// members (any more)
|
|
84
|
+
// @param rowsFor (headId) → the rows' template, or null when the family
|
|
85
|
+
// has no members (any more)
|
|
69
86
|
// @param anchorFor (headId) → its chip in the strip, or null
|
|
70
87
|
// @param onPick (id) a row's click
|
|
71
88
|
// @param onClose () after it closed (the strip's held order catches up)
|
|
@@ -73,7 +90,6 @@ export const PHONE_MAX_PX = 600;
|
|
|
73
90
|
export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor, onPick, onClose = () => {} }) {
|
|
74
91
|
let el = null;
|
|
75
92
|
let headId = null;
|
|
76
|
-
let html = "";
|
|
77
93
|
|
|
78
94
|
const element = () => {
|
|
79
95
|
if (el) return el;
|
|
@@ -110,10 +126,8 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
110
126
|
close();
|
|
111
127
|
return false;
|
|
112
128
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
html = rows;
|
|
116
|
-
}
|
|
129
|
+
// A row inserted before the focused one: the focus stays on its session.
|
|
130
|
+
keepFocus(el, "data-id", () => render(rows, el));
|
|
117
131
|
anchor.setAttribute("aria-expanded", "true");
|
|
118
132
|
place(anchor);
|
|
119
133
|
return true;
|
|
@@ -123,7 +137,6 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
123
137
|
if (headId && headId !== id) close();
|
|
124
138
|
headId = id;
|
|
125
139
|
element().hidden = false;
|
|
126
|
-
html = "";
|
|
127
140
|
draw();
|
|
128
141
|
}
|
|
129
142
|
|
|
@@ -131,9 +144,8 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
131
144
|
if (!headId) return;
|
|
132
145
|
anchorFor(headId)?.setAttribute("aria-expanded", "false");
|
|
133
146
|
headId = null;
|
|
134
|
-
html = "";
|
|
135
147
|
el.hidden = true;
|
|
136
|
-
el
|
|
148
|
+
render(nothing, el);
|
|
137
149
|
onClose();
|
|
138
150
|
}
|
|
139
151
|
|