@korso/shepherd-ui 0.1.1 → 0.2.1
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.
- package/dist/ShepherdRoot.d.ts +9 -0
- package/dist/ShepherdRoot.js +46 -0
- package/dist/client.d.ts +107 -0
- package/dist/client.js +238 -0
- package/dist/components/ActiveList.d.ts +22 -0
- package/dist/components/ActiveList.js +59 -0
- package/dist/components/Chat.d.ts +30 -0
- package/dist/components/Chat.js +61 -0
- package/dist/components/Composer.d.ts +37 -0
- package/dist/components/Composer.js +146 -0
- package/dist/components/Crew.d.ts +23 -0
- package/dist/components/Crew.js +31 -0
- package/dist/components/Dashboard.d.ts +48 -0
- package/dist/components/Dashboard.js +280 -0
- package/dist/components/DoneList.d.ts +29 -0
- package/dist/components/DoneList.js +50 -0
- package/dist/components/RepoSelect.d.ts +38 -0
- package/dist/components/RepoSelect.js +56 -0
- package/dist/components/Territory.d.ts +21 -0
- package/dist/components/Territory.js +21 -0
- package/dist/config/ConnectAgent.d.ts +10 -0
- package/dist/config/ConnectAgent.js +119 -0
- package/dist/config/EmptyState.d.ts +12 -0
- package/dist/config/EmptyState.js +8 -0
- package/dist/config/Members.d.ts +10 -0
- package/dist/config/Members.js +83 -0
- package/dist/config/Workspaces.d.ts +12 -0
- package/dist/config/Workspaces.js +96 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +6 -0
- package/dist/context.d.ts +28 -0
- package/dist/context.js +36 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +15 -0
- package/dist/lib/Dashboard-B7nioe5N.js +1286 -0
- package/dist/lib/index.d.ts +452 -16
- package/dist/lib/index.js +372 -6
- package/dist/lib/selfhost.js +9 -9
- package/dist/lib/styles.css +35 -0
- package/dist/logic.d.ts +208 -0
- package/dist/logic.js +372 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +11 -0
- package/dist/selfhost/assets/index-D6rTsbVM.js +40 -0
- package/dist/selfhost/assets/{index-tSyzirZa.css → index-DnhlP_lc.css} +1 -1
- package/dist/selfhost/index.html +2 -2
- package/dist/selfhost.d.ts +22 -0
- package/dist/selfhost.js +97 -0
- package/dist/test/mockClient.d.ts +6 -0
- package/dist/test/mockClient.js +45 -0
- package/dist/useLandscapePolling.d.ts +70 -0
- package/dist/useLandscapePolling.js +134 -0
- package/package.json +1 -1
- package/dist/lib/Dashboard-CB6SL-J2.js +0 -1064
- package/dist/selfhost/assets/index-BBx3vo2Y.js +0 -40
package/dist/logic.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, auth-agnostic page logic for the Shepherd wallboard.
|
|
3
|
+
*
|
|
4
|
+
* Ported verbatim from packages/hub/public/app.js (the original plain-JS single
|
|
5
|
+
* static page), adding TypeScript types only — behavior is byte-for-byte
|
|
6
|
+
* identical, as the characterization suite in test/logic.test.ts pins. These are
|
|
7
|
+
* the page's "Pure helpers" plus the "Glob containment + active-claim grouping"
|
|
8
|
+
* section; the browser-only DOM-wiring half of app.js is NOT ported here.
|
|
9
|
+
*
|
|
10
|
+
* Times are computed against the server's clock (`serverTime` in the payload),
|
|
11
|
+
* not the browser's, so countdowns and "last seen" stay correct under skew —
|
|
12
|
+
* hence every time helper takes an explicit `nowMs`/`endedIso` rather than
|
|
13
|
+
* reading `Date.now()`.
|
|
14
|
+
*
|
|
15
|
+
* Note: this module intentionally keeps its OWN browser-subset glob matcher
|
|
16
|
+
* (normalizeGlob/segmentCovers/patternCovers below); it does not import the
|
|
17
|
+
* server-side packages/hub/src/globs.ts, which is a deliberately-separate
|
|
18
|
+
* duplicate.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Human "N ago" for a past ISO timestamp, relative to `nowMs` (epoch ms).
|
|
22
|
+
*
|
|
23
|
+
* @param iso - A past instant as an ISO 8601 string.
|
|
24
|
+
* @param nowMs - The reference "now" in epoch milliseconds (the server clock).
|
|
25
|
+
* @returns A short relative label, e.g. `"just now"`, `"30s ago"`, `"2h ago"`.
|
|
26
|
+
*/
|
|
27
|
+
export function formatRelative(iso, nowMs) {
|
|
28
|
+
const secs = Math.floor((nowMs - Date.parse(iso)) / 1000);
|
|
29
|
+
if (secs < 5)
|
|
30
|
+
return "just now";
|
|
31
|
+
if (secs < 60)
|
|
32
|
+
return `${secs}s ago`;
|
|
33
|
+
const mins = Math.floor(secs / 60);
|
|
34
|
+
if (mins < 60)
|
|
35
|
+
return `${mins}m ago`;
|
|
36
|
+
const hrs = Math.floor(mins / 60);
|
|
37
|
+
if (hrs < 24)
|
|
38
|
+
return `${hrs}h ago`;
|
|
39
|
+
return `${Math.floor(hrs / 24)}d ago`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Human "expires in N" / "expired" for a future ISO timestamp, relative to
|
|
43
|
+
* `nowMs`.
|
|
44
|
+
*
|
|
45
|
+
* @param iso - The expiry instant as an ISO 8601 string.
|
|
46
|
+
* @param nowMs - The reference "now" in epoch milliseconds (the server clock).
|
|
47
|
+
* @returns `"expired"` once at/past the instant, else e.g. `"expires in 4m"`.
|
|
48
|
+
*/
|
|
49
|
+
export function formatCountdown(iso, nowMs) {
|
|
50
|
+
const secs = Math.floor((Date.parse(iso) - nowMs) / 1000);
|
|
51
|
+
if (secs <= 0)
|
|
52
|
+
return "expired";
|
|
53
|
+
if (secs < 60)
|
|
54
|
+
return `expires in ${secs}s`;
|
|
55
|
+
const mins = Math.floor(secs / 60);
|
|
56
|
+
if (mins < 60)
|
|
57
|
+
return `expires in ${mins}m`;
|
|
58
|
+
return `expires in ${Math.floor(mins / 60)}h`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Deterministic chat color for an agent name — same name always maps to the
|
|
62
|
+
* same hue, so each speaker reads consistently across the announcement thread.
|
|
63
|
+
* Saturation/lightness are tuned to stay legible on the dark background.
|
|
64
|
+
*
|
|
65
|
+
* @param name - The agent name (may be empty, which yields hue 0 — never throws).
|
|
66
|
+
* @returns An `hsl(...)` CSS color string.
|
|
67
|
+
*/
|
|
68
|
+
export function colorForName(name) {
|
|
69
|
+
let h = 0;
|
|
70
|
+
for (let i = 0; i < name.length; i++)
|
|
71
|
+
h = (h * 31 + name.charCodeAt(i)) % 360;
|
|
72
|
+
return `hsl(${h}, 38%, 42%)`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Up to two initials for an avatar. Prefers the capital letters of a CamelCase
|
|
76
|
+
* agent name (RedDragon → RD); otherwise the first two letters (alice → AL).
|
|
77
|
+
*
|
|
78
|
+
* @param name - The agent name; empty yields the `"?"` placeholder.
|
|
79
|
+
* @returns A 1–2 character uppercase label.
|
|
80
|
+
*/
|
|
81
|
+
export function initialsFor(name) {
|
|
82
|
+
if (!name)
|
|
83
|
+
return "?";
|
|
84
|
+
const caps = name.match(/[A-Z]/g);
|
|
85
|
+
if (caps && caps.length >= 2)
|
|
86
|
+
return caps[0] + caps[1];
|
|
87
|
+
return name.slice(0, 2).toUpperCase();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Characters allowed in an agent-name mention token. Agent names are CamelCase
|
|
91
|
+
* (`RedDragon`) or `handle-ordinal` (`daichi-6`), so letters, digits, hyphen,
|
|
92
|
+
* and underscore. Used by both the autocomplete and target extraction.
|
|
93
|
+
*/
|
|
94
|
+
const MENTION_CHARS = "A-Za-z0-9_-";
|
|
95
|
+
/**
|
|
96
|
+
* The active `@mention` immediately to the LEFT of the caret, or null when the
|
|
97
|
+
* caret isn't in one. Drives the autocomplete: the `@` must start the text or
|
|
98
|
+
* follow whitespace (so email addresses like `a@b` don't trigger it), and only
|
|
99
|
+
* mention-name characters may sit between it and the caret.
|
|
100
|
+
*
|
|
101
|
+
* @param text - The full input text.
|
|
102
|
+
* @param caretIndex - The caret position within `text`.
|
|
103
|
+
* @returns The mention slice and query, or `null` when not in a mention.
|
|
104
|
+
*/
|
|
105
|
+
export function parseMention(text, caretIndex) {
|
|
106
|
+
const before = text.slice(0, caretIndex);
|
|
107
|
+
const m = before.match(new RegExp(`(?:^|\\s)@([${MENTION_CHARS}]*)$`));
|
|
108
|
+
if (!m)
|
|
109
|
+
return null;
|
|
110
|
+
const query = m[1];
|
|
111
|
+
return { start: caretIndex - query.length - 1, end: caretIndex, query };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The first `@mention` in `text` that matches a known agent name, returned in
|
|
115
|
+
* the name's canonical casing (so `@reddragon` resolves to `RedDragon`), or null
|
|
116
|
+
* when none match. This is what turns a typed message into a directed DM; an
|
|
117
|
+
* unmatched `@foo` is treated as plain text and the message broadcasts.
|
|
118
|
+
*
|
|
119
|
+
* @param text - The message text to scan.
|
|
120
|
+
* @param knownNames - Canonical agent names to resolve against (case-insensitive).
|
|
121
|
+
* @returns The canonical name of the first matching mention, or `null`.
|
|
122
|
+
*/
|
|
123
|
+
export function extractTarget(text, knownNames) {
|
|
124
|
+
const re = new RegExp(`(?:^|\\s)@([${MENTION_CHARS}]+)`, "g");
|
|
125
|
+
let m;
|
|
126
|
+
while ((m = re.exec(text)) !== null) {
|
|
127
|
+
const typed = m[1].toLowerCase();
|
|
128
|
+
const hit = knownNames.find((n) => n.toLowerCase() === typed);
|
|
129
|
+
if (hit)
|
|
130
|
+
return hit;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Sorted unique repo identifiers present across the given tasks.
|
|
136
|
+
*
|
|
137
|
+
* @param tasks - Items carrying a `repo` (a structural subset of {@link WorkspaceTaskT}).
|
|
138
|
+
* @returns The distinct repos, ascending.
|
|
139
|
+
*/
|
|
140
|
+
export function distinctRepos(tasks) {
|
|
141
|
+
return [...new Set(tasks.map((t) => t.repo))].sort();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* True if `item.repo` matches the selection; `null`/`"__all__"` = all repos.
|
|
145
|
+
*
|
|
146
|
+
* @param item - Anything carrying a `repo`.
|
|
147
|
+
* @param selected - The selected repo, or `null`/`"__all__"` for all.
|
|
148
|
+
* @returns Whether the item belongs to the current board filter.
|
|
149
|
+
*/
|
|
150
|
+
export function matchesRepo(item, selected) {
|
|
151
|
+
return selected === null || selected === "__all__" || item.repo === selected;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Live agents addressable by @mention under the current board filter: only those
|
|
155
|
+
* whose (most-recent) session sits in the selected repo, so a message can't be
|
|
156
|
+
* directed at someone who isn't in the repo you're viewing. Mirrors the crew the
|
|
157
|
+
* board already shows for that repo; in All-repos mode everyone live is
|
|
158
|
+
* addressable. Returns de-duplicated canonical names, sorted.
|
|
159
|
+
*
|
|
160
|
+
* @param agents - Agents carrying `name`, `presence`, and `repo` (a structural
|
|
161
|
+
* subset of {@link WorkspaceAgentT}).
|
|
162
|
+
* @param selectedRepo - The selected repo, or `null`/`"__all__"` for all.
|
|
163
|
+
* @returns Sorted, de-duplicated names of mentionable live agents.
|
|
164
|
+
*/
|
|
165
|
+
export function mentionableAgents(agents, selectedRepo) {
|
|
166
|
+
return [
|
|
167
|
+
...new Set(agents
|
|
168
|
+
.filter((a) => a.presence === "live" && matchesRepo(a, selectedRepo))
|
|
169
|
+
.map((a) => a.name)),
|
|
170
|
+
].sort();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* First-load default repo: newest active task's repo, else newest task's, else
|
|
174
|
+
* null.
|
|
175
|
+
*
|
|
176
|
+
* @param tasks - Tasks carrying `repo` and `status` (a structural subset of
|
|
177
|
+
* {@link WorkspaceTaskT}); assumed newest-first.
|
|
178
|
+
* @returns The repo to select on first load, or `null` when there are no tasks.
|
|
179
|
+
*/
|
|
180
|
+
export function defaultRepo(tasks) {
|
|
181
|
+
const active = tasks.find((t) => t.status === "active");
|
|
182
|
+
if (active)
|
|
183
|
+
return active.repo;
|
|
184
|
+
return tasks.length ? tasks[0].repo : null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Human label for a history task's status.
|
|
188
|
+
*
|
|
189
|
+
* @param status - The task status.
|
|
190
|
+
* @returns `"dropped"` for a dropped task, otherwise `"done"`.
|
|
191
|
+
*/
|
|
192
|
+
export function statusLabel(status) {
|
|
193
|
+
return status === "dropped" ? "dropped" : "done";
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* "active Nm/Nh" for the created->ended span; "" when the task hasn't ended.
|
|
197
|
+
*
|
|
198
|
+
* @param createdIso - The task's creation instant (ISO 8601 string).
|
|
199
|
+
* @param endedIso - The task's end instant, or `null` while still active.
|
|
200
|
+
* @returns A duration label, or the empty string for an unfinished task.
|
|
201
|
+
*/
|
|
202
|
+
export function formatActiveDuration(createdIso, endedIso) {
|
|
203
|
+
if (!endedIso)
|
|
204
|
+
return "";
|
|
205
|
+
const secs = Math.floor((Date.parse(endedIso) - Date.parse(createdIso)) / 1000);
|
|
206
|
+
const mins = Math.floor(secs / 60);
|
|
207
|
+
if (mins < 60)
|
|
208
|
+
return `active ${mins}m`;
|
|
209
|
+
return `active ${Math.floor(mins / 60)}h`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Local-day bucket label: "Today" / "Yesterday" / "Mon D".
|
|
213
|
+
*
|
|
214
|
+
* @param iso - The instant to bucket (ISO 8601 string).
|
|
215
|
+
* @param nowMs - The reference "now" in epoch milliseconds.
|
|
216
|
+
* @returns `"Today"`, `"Yesterday"`, or a short month/day label.
|
|
217
|
+
*/
|
|
218
|
+
export function dayBucket(iso, nowMs) {
|
|
219
|
+
const d = new Date(Date.parse(iso));
|
|
220
|
+
const now = new Date(nowMs);
|
|
221
|
+
const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
222
|
+
const dayDiff = Math.round((startOf(now) - startOf(d)) / 86400000);
|
|
223
|
+
if (dayDiff <= 0)
|
|
224
|
+
return "Today";
|
|
225
|
+
if (dayDiff === 1)
|
|
226
|
+
return "Yesterday";
|
|
227
|
+
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Glob containment + active-claim grouping
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
/**
|
|
233
|
+
* Normalize a glob into a segment list. Mirrors the server's normalize
|
|
234
|
+
* (packages/hub/src/globs.ts) so the two agree on path shape: lowercase,
|
|
235
|
+
* backslashes -> '/', a trailing slash claims the subtree ('dir/' -> 'dir/**'),
|
|
236
|
+
* split on '/', and resolve '.'/'..'. Module-private.
|
|
237
|
+
*/
|
|
238
|
+
function normalizeGlob(pattern) {
|
|
239
|
+
let p = pattern.toLowerCase().replace(/\\/g, "/");
|
|
240
|
+
if (p.endsWith("/"))
|
|
241
|
+
p = p.replace(/\/+$/, "") + "/**";
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const seg of p.split("/")) {
|
|
244
|
+
if (seg === "" || seg === ".")
|
|
245
|
+
continue;
|
|
246
|
+
if (seg === "..") {
|
|
247
|
+
if (out.length)
|
|
248
|
+
out.pop();
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
out.push(seg);
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
const SEG_WILDCARD = /[*?{}[\]]/;
|
|
256
|
+
/**
|
|
257
|
+
* Does single-segment pattern `a` cover single-segment pattern `b`? Conservative:
|
|
258
|
+
* '*' covers any one segment; a literal covers only the identical literal; any
|
|
259
|
+
* other wildcard ('?', braces, brackets) covers only a byte-identical segment.
|
|
260
|
+
* Module-private.
|
|
261
|
+
*/
|
|
262
|
+
function segmentCovers(a, b) {
|
|
263
|
+
if (a === "*")
|
|
264
|
+
return true;
|
|
265
|
+
if (SEG_WILDCARD.test(a))
|
|
266
|
+
return a === b;
|
|
267
|
+
return !SEG_WILDCARD.test(b) && a === b;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Does pattern A (segments) cover pattern B (segments) — i.e. every concrete path
|
|
271
|
+
* matching B also matches A? '**' in A consumes zero-or-more B segments; a bounded
|
|
272
|
+
* (non-'**') A segment can NEVER cover a B '**' (the safety gate that stops a
|
|
273
|
+
* shallow claim from swallowing a deep one). Memoized on the (i, j) suffix.
|
|
274
|
+
* Module-private.
|
|
275
|
+
*/
|
|
276
|
+
function patternCovers(a, b) {
|
|
277
|
+
const memo = new Map();
|
|
278
|
+
function go(i, j) {
|
|
279
|
+
if (b.length - j === 0) {
|
|
280
|
+
// B exhausted: A covers iff its remainder matches the empty tail, i.e.
|
|
281
|
+
// every remaining A segment is '**' (which can consume zero segments).
|
|
282
|
+
for (let k = i; k < a.length; k++)
|
|
283
|
+
if (a[k] !== "**")
|
|
284
|
+
return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
if (a.length - i === 0)
|
|
288
|
+
return false; // A exhausted, B still has depth
|
|
289
|
+
const key = i * (b.length + 1) + j;
|
|
290
|
+
const cached = memo.get(key);
|
|
291
|
+
if (cached !== undefined)
|
|
292
|
+
return cached;
|
|
293
|
+
const ah = a[i], bh = b[j];
|
|
294
|
+
let res;
|
|
295
|
+
if (ah === "**") {
|
|
296
|
+
res = go(i + 1, j) || go(i, j + 1); // consume zero, or one B segment
|
|
297
|
+
}
|
|
298
|
+
else if (bh === "**") {
|
|
299
|
+
res = false; // bounded A segment cannot cover an unbounded B '**'
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
res = segmentCovers(ah, bh) && go(i + 1, j + 1);
|
|
303
|
+
}
|
|
304
|
+
memo.set(key, res);
|
|
305
|
+
return res;
|
|
306
|
+
}
|
|
307
|
+
return go(0, 0);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Does glob set `outer` fully cover glob set `inner` — every path `inner` could
|
|
311
|
+
* touch already inside `outer`? Conservative: unsure -> false, so a caller only
|
|
312
|
+
* folds a claim it's certain is contained (a false negative just shows both
|
|
313
|
+
* claims, which is safe). An empty `inner` is trivially covered.
|
|
314
|
+
*
|
|
315
|
+
* @param outer - The candidate broader glob set.
|
|
316
|
+
* @param inner - The candidate narrower glob set.
|
|
317
|
+
* @returns Whether every glob in `inner` is covered by some glob in `outer`.
|
|
318
|
+
*/
|
|
319
|
+
export function globsCover(outer, inner) {
|
|
320
|
+
if (inner.length === 0)
|
|
321
|
+
return true;
|
|
322
|
+
const o = outer.map(normalizeGlob);
|
|
323
|
+
return inner.every((g) => {
|
|
324
|
+
const ng = normalizeGlob(g);
|
|
325
|
+
return o.some((op) => patternCovers(op, ng));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Group active claims by agent for the board. Within an agent's claims, one that
|
|
330
|
+
* is STRICTLY covered by a broader sibling (the sibling covers it but not vice
|
|
331
|
+
* versa, so identical claims don't cannibalise each other) folds into `narrower`;
|
|
332
|
+
* everything else stays a visible `primary`. Distinct or merely-overlapping
|
|
333
|
+
* claims all remain primaries. Pure — no DOM, no clock; `createdAt` drives order.
|
|
334
|
+
*
|
|
335
|
+
* Groups are newest-claim-first; `primaries`/`narrower` are each newest-first;
|
|
336
|
+
* header fields (model/program/repo) come from the group's newest claim.
|
|
337
|
+
*
|
|
338
|
+
* @param tasks - The active claims to group (a structural subset of
|
|
339
|
+
* {@link WorkspaceTaskT}).
|
|
340
|
+
* @returns One {@link ClaimGroup} per agent, newest-claim-first.
|
|
341
|
+
*/
|
|
342
|
+
export function groupActiveClaims(tasks) {
|
|
343
|
+
const byAgent = new Map();
|
|
344
|
+
for (const t of tasks) {
|
|
345
|
+
if (!byAgent.has(t.agentName))
|
|
346
|
+
byAgent.set(t.agentName, []);
|
|
347
|
+
byAgent.get(t.agentName).push(t);
|
|
348
|
+
}
|
|
349
|
+
const newestFirst = (a, b) => b.createdAt.localeCompare(a.createdAt);
|
|
350
|
+
const groups = [];
|
|
351
|
+
for (const [agentName, claims] of byAgent) {
|
|
352
|
+
const sorted = [...claims].sort(newestFirst);
|
|
353
|
+
const isNarrower = sorted.map((c) => sorted.some((o) => o !== c &&
|
|
354
|
+
globsCover(o.pathGlobs, c.pathGlobs) &&
|
|
355
|
+
!globsCover(c.pathGlobs, o.pathGlobs)));
|
|
356
|
+
const head = sorted[0];
|
|
357
|
+
groups.push({
|
|
358
|
+
agentName,
|
|
359
|
+
model: head.model,
|
|
360
|
+
program: head.program,
|
|
361
|
+
repo: head.repo,
|
|
362
|
+
primaries: sorted.filter((_, i) => !isNarrower[i]),
|
|
363
|
+
narrower: sorted.filter((_, i) => isNarrower[i]),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
groups.sort((g1, g2) => {
|
|
367
|
+
const t1 = g1.primaries.concat(g1.narrower)[0].createdAt;
|
|
368
|
+
const t2 = g2.primaries.concat(g2.narrower)[0].createdAt;
|
|
369
|
+
return t2.localeCompare(t1);
|
|
370
|
+
});
|
|
371
|
+
return groups;
|
|
372
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./styles.css";
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { StrictMode } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import "./styles.css";
|
|
5
|
+
import { SelfHostApp } from "./selfhost.js";
|
|
6
|
+
/**
|
|
7
|
+
* SPA mount point for the self-host app build. Bundles the stylesheet (the
|
|
8
|
+
* standalone build ships its own CSS, unlike the `.` lib export which leaves
|
|
9
|
+
* styling opt-in) and mounts the token-gated {@link SelfHostApp} at #root.
|
|
10
|
+
*/
|
|
11
|
+
createRoot(document.getElementById("root")).render(_jsx(StrictMode, { children: _jsx(SelfHostApp, {}) }));
|