@ra3orblade/swarm 0.5.0 → 0.6.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.
- package/dist/swarm-mcp.js +24 -1
- package/dist/swarm.js +93 -7
- package/dist/swarmd.js +919 -161
- package/package.json +1 -1
- package/web/app.js +282 -8
- package/web/icons.js +2 -2
- package/web/index.html +29 -0
- package/web/release-notes.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ra3orblade/swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Local-first control plane for AI-agent development: watch every Claude Code / Codex / Grok session on your machine, ledger tasks and worktrees, enforce rules as hook denials.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/web/app.js
CHANGED
|
@@ -45,7 +45,7 @@ document.addEventListener("keydown", (ev) => {
|
|
|
45
45
|
window.swarmZoom(dir);
|
|
46
46
|
});
|
|
47
47
|
// `dirty`: a UI-side change (selection, view, filter) needs a render even when the daemon snapshot is unchanged.
|
|
48
|
-
const state = { projects: [], sessions: [], worktrees: {}, processes: [], spend: null, incidents: [], allIncidents: null, incFilter: "open", tasks: null, gates: null, runs: [], taskFilter: "ready", resources: [], prs: [], seq: 0, sel: null, session: null, log: [], turns: [], view: "fleet", agentFilter: null, dirty: true };
|
|
48
|
+
const state = { projects: [], sessions: [], worktrees: {}, processes: [], spend: null, incidents: [], allIncidents: null, incFilter: "open", tasks: null, gates: null, runs: [], attribution: null, taskFilter: "ready", resources: [], prs: [], seq: 0, sel: null, session: null, log: [], turns: [], view: "fleet", agentFilter: null, dirty: true };
|
|
49
49
|
|
|
50
50
|
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
51
51
|
const ago = (iso) => { const d = (Date.now() - new Date(iso)) / 1000; return d < 60 ? `${d | 0}s` : d < 3600 ? `${(d / 60) | 0}m` : d < 86400 ? `${(d / 3600) | 0}h` : `${(d / 86400) | 0}d`; };
|
|
@@ -129,13 +129,22 @@ async function refresh() {
|
|
|
129
129
|
const txt = await (await fetch("/v1/state")).text();
|
|
130
130
|
const same = txt === lastSnap;
|
|
131
131
|
if (!same) { lastSnap = txt; Object.assign(state, JSON.parse(txt)); }
|
|
132
|
-
if (!state.version) fetch("/v1/health").then((r) => r.json()).then((h) => { state.version = h.version; }).catch(() => {});
|
|
132
|
+
if (!state.version) fetch("/v1/health").then((r) => r.json()).then((h) => { state.version = h.version; maybeWhatsNew(); }).catch(() => {});
|
|
133
133
|
let prsChanged = false;
|
|
134
134
|
if (state.view === "prs" && !state.session) {
|
|
135
135
|
const prs = await (await fetch("/v1/prs")).json().catch(() => state.prs ?? []);
|
|
136
136
|
prsChanged = JSON.stringify(prs) !== JSON.stringify(state.prs);
|
|
137
137
|
state.prs = prs;
|
|
138
138
|
}
|
|
139
|
+
let attrChanged = false;
|
|
140
|
+
if (state.view === "spend" && state.sel && !state.session) {
|
|
141
|
+
const a = await fetch(`/v1/attribution?project=${encodeURIComponent(state.sel)}`).then((r) => r.json()).catch(() => state.attribution);
|
|
142
|
+
attrChanged = JSON.stringify(a) !== JSON.stringify(state.attribution);
|
|
143
|
+
state.attribution = a;
|
|
144
|
+
} else if (state.view === "spend" && !state.sel) {
|
|
145
|
+
if (state.attribution) attrChanged = true;
|
|
146
|
+
state.attribution = null;
|
|
147
|
+
}
|
|
139
148
|
let runsChanged = false;
|
|
140
149
|
const openSpawned = state.session && state.sessions.find((x) => x.id === state.session)?.kind === "spawned";
|
|
141
150
|
if (openSpawned || (state.view === "board" && !state.session) || (state.view === "fleet" && !state.session)) {
|
|
@@ -159,9 +168,9 @@ async function refresh() {
|
|
|
159
168
|
incChanged = JSON.stringify(inc) !== JSON.stringify(state.allIncidents);
|
|
160
169
|
state.allIncidents = inc;
|
|
161
170
|
}
|
|
162
|
-
if (!same || prsChanged || incChanged || tasksChanged || runsChanged || state.dirty || Date.now() - lastRenderAt > 30_000) schedule();
|
|
171
|
+
if (!same || prsChanged || incChanged || tasksChanged || runsChanged || attrChanged || state.dirty || Date.now() - lastRenderAt > 30_000) schedule();
|
|
163
172
|
}
|
|
164
|
-
const VIEWS = ["fleet", "board", "incidents", "prs", "timeline", "spend", "stats"];
|
|
173
|
+
const VIEWS = ["fleet", "board", "incidents", "prs", "timeline", "spend", "stats", "search"];
|
|
165
174
|
// restore last view + project selection (persisted UI state)
|
|
166
175
|
{
|
|
167
176
|
const v = localStorage.getItem("swarm.view");
|
|
@@ -186,6 +195,7 @@ function render() {
|
|
|
186
195
|
if (state.session) renderSession();
|
|
187
196
|
else if (state.view === "spend") renderSpend();
|
|
188
197
|
else if (state.view === "stats") { loadStats(); renderStats(); } // loadStats is a no-op while the cache is fresh
|
|
198
|
+
else if (state.view === "search") renderSearch();
|
|
189
199
|
else if (state.view === "timeline") renderTimeline();
|
|
190
200
|
else if (state.view === "board") renderBoard();
|
|
191
201
|
else if (state.view === "incidents") renderIncidentsView();
|
|
@@ -397,7 +407,64 @@ function renderIncidents() {
|
|
|
397
407
|
});
|
|
398
408
|
}
|
|
399
409
|
|
|
410
|
+
// M4.6: rule dry-run — replay this project's history under chosen modes; nothing is recorded.
|
|
411
|
+
const RULE_IDS = ["pattern_kill", "shared_tree", "destructive_git", "protected_ports", "no_foreign_worktree", "claim_required_to_write"];
|
|
412
|
+
const dry = { modes: {}, report: null, busy: false };
|
|
413
|
+
async function openDryRun() {
|
|
414
|
+
if (!state.sel) return alert("Pick a project in the sidebar first — the dry-run replays one project's history.");
|
|
415
|
+
dry.modes = {}; dry.report = null;
|
|
416
|
+
await runDryRun();
|
|
417
|
+
}
|
|
418
|
+
async function runDryRun() {
|
|
419
|
+
dry.busy = true; renderDryRun();
|
|
420
|
+
const q = new URLSearchParams({ project: state.sel, ...dry.modes });
|
|
421
|
+
dry.report = await fetch(`/v1/rules/dryrun?${q}`).then((r) => r.json()).catch((e) => ({ ok: false, error: String(e) }));
|
|
422
|
+
dry.busy = false; renderDryRun();
|
|
423
|
+
}
|
|
424
|
+
function renderDryRun() {
|
|
425
|
+
const r = dry.report;
|
|
426
|
+
const sel = (id) => {
|
|
427
|
+
const cur = dry.modes[id] ?? r?.modes?.[id] ?? "ask";
|
|
428
|
+
return `<label class="dr-rule"><span class="br">${id}</span><select data-drmode="${id}">${["ask", "deny", "off"].map((m) => `<option value="${m}" ${m === cur ? "selected" : ""}>${m}</option>`).join("")}</select>${r ? `<span class="dim">ask <b>${r.byRule[id].ask}</b> · deny <b>${r.byRule[id].deny}</b></span>` : ""}</label>`;
|
|
429
|
+
};
|
|
430
|
+
const flaky = (r?.flaky ?? []).map((f) => `<div class="dr-flaky"><code>${esc(f.display)}</code><div class="dim" style="font-size:var(--fs-sm)">${esc(f.suggestion)} · ${f.sessions} session${f.sessions === 1 ? "" : "s"}</div></div>`).join("");
|
|
431
|
+
const hits = (r?.hits ?? []).slice(-40).reverse().map((h) => `<tr><td class="dim">${hhmm(h.ts)}</td><td><span class="br">${esc(h.rule)}</span></td><td>${h.action}</td><td><code>${esc(h.display)}</code></td><td class="dim">${h.completed ? "ran" : ""}</td></tr>`).join("");
|
|
432
|
+
$("#picker").innerHTML = `<div class="pk wn" role="dialog" aria-modal="true">
|
|
433
|
+
<div class="pk-h">${ic("shield", 15)}<b>Rule dry-run</b><span class="dim" style="margin-left:8px">${esc(projName(state.sel))}</span><span class="grow"></span><button id="pkCancel" title="Close">${ic("x", 14)}</button></div>
|
|
434
|
+
<div class="pk-b">
|
|
435
|
+
<p class="dim" style="font-size:var(--fs-sm)">Replays this project's recorded tool calls through the rules under the modes below — what <em>would</em> have been asked or denied. Nothing is recorded; change a mode and re-run to try a rule before switching it on in <code>.swarm.toml</code>.</p>
|
|
436
|
+
<div class="dr-rules">${RULE_IDS.map(sel).join("")}</div>
|
|
437
|
+
${dry.busy ? '<p class="dim">replaying…</p>' : r?.error ? `<p class="dim">${esc(r.error)}</p>` : r ? `
|
|
438
|
+
<div class="date">${r.evaluated} of ${r.calls} calls evaluated · ${r.hits.length}${r.hits.length >= 200 ? "+" : ""} hits</div>
|
|
439
|
+
<h4>Flaky signals <span class="dim">rules that keep asking about something that is then allowed anyway</span></h4>
|
|
440
|
+
${flaky || '<p class="dim" style="font-size:var(--fs-sm)">None — every rule that fired stuck.</p>'}
|
|
441
|
+
<h4>Would have fired <span class="dim">newest first, last 40</span></h4>
|
|
442
|
+
${hits ? `<div style="overflow-x:auto"><table class="plain"><tbody>${hits}</tbody></table></div>` : '<p class="dim" style="font-size:var(--fs-sm)">Nothing — these modes are silent on this history.</p>'}` : ""}
|
|
443
|
+
</div>
|
|
444
|
+
<div class="pk-f"><span class="grow"></span><button id="drRun" ${dry.busy ? "disabled" : ""}>Re-run</button><button id="pkCancel">Close</button></div>
|
|
445
|
+
</div>`;
|
|
446
|
+
}
|
|
447
|
+
|
|
400
448
|
// ---------- incidents view (M2.3): the denied-action feed, with ack
|
|
449
|
+
// M4.3: turn an incident into a .swarm.toml rule + a CLAUDE.md lesson, both copyable.
|
|
450
|
+
function codifyIncident(seq) {
|
|
451
|
+
const i = (state.allIncidents ?? []).find((x) => x.seq === Number(seq));
|
|
452
|
+
if (!i?.suggestion) return;
|
|
453
|
+
const sg = i.suggestion;
|
|
454
|
+
$("#picker").innerHTML = `<div class="pk wn" role="dialog" aria-modal="true">
|
|
455
|
+
<div class="pk-h">${ic("shield", 15)}<b>Codify</b><span class="grow"></span><button id="pkCancel" title="Close">${ic("x", 14)}</button></div>
|
|
456
|
+
<div class="pk-b">
|
|
457
|
+
<h3>${esc(sg.title)}</h3>
|
|
458
|
+
<div class="date">from a <span class="br">${esc(i.rule)}</span> incident${i.count > 1 ? ` \u00b7 seen ${i.count}\u00d7` : ""}</div>
|
|
459
|
+
${sg.toml ? `<h4>.swarm.toml <a href="#" class="cbtn" data-copy-toml="${seq}">${ic("copy", 12)} copy</a></h4><pre class="snip" id="toml-${seq}">${esc(sg.toml)}</pre>` : ""}
|
|
460
|
+
<h4>CLAUDE.md lesson <a href="#" class="cbtn" data-copy-lesson="${seq}">${ic("copy", 12)} copy</a></h4>
|
|
461
|
+
<pre class="snip" id="lesson-${seq}">- ${esc(sg.lesson)}</pre>
|
|
462
|
+
${sg.toml ? '<p class="dim" style="font-size:var(--fs-sm)">Merge the block into the repo\'s <code>.swarm.toml</code>; the daemon picks it up within ~30s.</p>' : '<p class="dim" style="font-size:var(--fs-sm)">No config rule fits this one \u2014 the lesson is the takeaway.</p>'}
|
|
463
|
+
</div>
|
|
464
|
+
<div class="pk-f"><span class="grow"></span><button id="pkCancel">Close</button></div>
|
|
465
|
+
</div>`;
|
|
466
|
+
}
|
|
467
|
+
|
|
401
468
|
function renderIncidentsView() {
|
|
402
469
|
const all = state.allIncidents;
|
|
403
470
|
const rows = (all ?? []).filter((i) => !state.sel || i.projectId === state.sel);
|
|
@@ -408,14 +475,14 @@ function renderIncidentsView() {
|
|
|
408
475
|
const rules = [...byRule.entries()].sort((a, b) => b[1] - a[1]).map(([r, n]) => `<span class="br">${esc(r)}</span> <b>${n}</b>`).join(" · ");
|
|
409
476
|
$("#main").innerHTML =
|
|
410
477
|
`<h2>Incidents <span>${all === null ? "loading…" : `${open} open · ${rows.length} shown`} · every ask/deny the rules made${rules ? ` · ${rules}` : ""}</span></h2>` +
|
|
411
|
-
`<div class="chips">${chip("open", "Open")}${chip("all", "All")}${open ? `<span class="chip" data-ackall="1" title="Mark every open incident${state.sel ? " in this project" : ""} as seen">Ack all <b>${open}</b></span>` : ""}</div>` +
|
|
478
|
+
`<div class="chips">${chip("open", "Open")}${chip("all", "All")}${open ? `<span class="chip" data-ackall="1" title="Mark every open incident${state.sel ? " in this project" : ""} as seen">Ack all <b>${open}</b></span>` : ""}${state.sel ? `<span class="chip" id="dryrun" title="Replay this project's history under different rule modes">${ic("shield", 12)} Dry-run rules</span>` : ""}</div>` +
|
|
412
479
|
(rows.length
|
|
413
480
|
? dataTable({
|
|
414
481
|
id: "incidents-feed",
|
|
415
482
|
columns: incidentColumns(true),
|
|
416
483
|
rows,
|
|
417
484
|
leading: { width: 24, cell: incidentDot },
|
|
418
|
-
trailing: { width:
|
|
485
|
+
trailing: { width: 120, cell: (i) => `${i.suggestion ? `<a href="#" data-codify="${i.seq}" title="Turn this into a rule / lesson">${ic("shield", 12)} Codify</a> ` : ""}${ackLink(i)}` },
|
|
419
486
|
rowAttrs: () => "",
|
|
420
487
|
rerender: touch,
|
|
421
488
|
})
|
|
@@ -528,7 +595,8 @@ function renderTasks() {
|
|
|
528
595
|
{ key: "state", label: "state", width: 150, get: (t) => (t.claimedBy ? 0 : t.ready ? 1 : t.status === "active" ? 2 : t.status === "done" ? 4 : 3), cell: st },
|
|
529
596
|
...(hasGates ? [{ key: "gates", label: "gates", width: 170, get: (t) => (t.gates ?? []).filter((g) => g.verdict === "pass").length, cell: (t) => gateChips(t.gates ?? []) }] : []),
|
|
530
597
|
];
|
|
531
|
-
|
|
598
|
+
const srcLabel = state.tasks.source === "github" ? "GitHub Issues" : state.tasks.source === "linear" ? "Linear" : state.tasks.source;
|
|
599
|
+
return `<h2 class="mt-sec">Tasks <span>${ready.length} ready · ${all.length} in ${esc(srcLabel)}${state.tasks.error ? ` · <span class="badge warn" title="${esc(state.tasks.error)}">${ic("warning", 12)} ${esc(state.tasks.error)}</span>` : ""}</span></h2>` +
|
|
532
600
|
`<div class="chips">${chip("ready", "Ready", ready.length)}${chip("open", "Open", all.filter((t) => t.status !== "done").length)}${chip("all", "All", all.length)}</div>` +
|
|
533
601
|
(rows.length
|
|
534
602
|
? dataTable({
|
|
@@ -664,9 +732,55 @@ function renderSpend() {
|
|
|
664
732
|
<div class="cols mt-sec"><div><h2>By project · today <span>${usd(sumBy(filt(sp.byProjectToday), (x) => x.cost))}</span></h2>${tbl(filt(sp.byProjectToday), "project", projName)}
|
|
665
733
|
<h2 class="mt-sec">By project · all time</h2>${tbl(filt(sp.byProjectAll), "project", projName)}</div>
|
|
666
734
|
<div>${byAgentToday ? `<h2>By model · today</h2>${tbl(sp.byModelToday, "model", model)}` : ""}<h2 style="${byAgentToday ? "margin-top:18px" : ""}">By model · all time</h2>${tbl(sp.byModelAll, "model", model)}</div></div>
|
|
735
|
+
${renderAttribution()}
|
|
667
736
|
<p class="dim" style="margin-top:var(--gap-sec)">Costs use list prices (static table, refreshed from LiteLLM when online; override in <code>~/.swarm/pricing.json</code>). Cache reads are the bulk of "ctx". Sessions on a subscription plan still show what the tokens would cost at API rates.</p>`;
|
|
668
737
|
}
|
|
669
738
|
|
|
739
|
+
// M4.2: cost attributed to tasks (via each claim's worktree) + a context re-processing signal.
|
|
740
|
+
// Only meaningful with a project selected.
|
|
741
|
+
function renderAttribution() {
|
|
742
|
+
const a = state.attribution;
|
|
743
|
+
if (!state.sel || !a) return "";
|
|
744
|
+
const parts = [];
|
|
745
|
+
if (a.byTask?.length) {
|
|
746
|
+
parts.push(`<h2 class="mt-sec">By task <span>${usd(sumBy(a.byTask, (t) => t.cost))} across ${a.byTask.length} task${a.byTask.length === 1 ? "" : "s"} · attributed by worktree</span></h2>` +
|
|
747
|
+
dataTable({
|
|
748
|
+
id: "spend-task",
|
|
749
|
+
columns: [
|
|
750
|
+
{ key: "task", label: "task", width: 150, get: (t) => t.task, cell: (t) => `<b>${esc(t.task)}</b>` },
|
|
751
|
+
{ key: "owner", label: "owner", width: 120, get: (t) => t.owner || "", cell: (t) => esc(t.owner || "—") },
|
|
752
|
+
{ key: "cost", label: "cost", width: 88, num: true, get: (t) => t.cost, cell: (t) => usd(t.cost) },
|
|
753
|
+
{ key: "output", label: "out", width: 84, num: true, get: (t) => t.output, cell: (t) => tok(t.output) },
|
|
754
|
+
{ key: "sessions", label: "sessions", width: 84, num: true, get: (t) => t.sessions, cell: (t) => String(t.sessions) },
|
|
755
|
+
{ key: "turns", label: "turns", width: 64, num: true, get: (t) => t.turns, cell: (t) => String(t.turns) },
|
|
756
|
+
{ key: "worktree", label: "worktree", flex: true, get: (t) => t.worktree, cell: (t) => `<span class="now dim" title="${esc(t.worktree)}">${esc(short(t.worktree))}</span>` },
|
|
757
|
+
],
|
|
758
|
+
rows: a.byTask,
|
|
759
|
+
leading: { width: 20, cell: () => "" },
|
|
760
|
+
trailing: { width: 8, cell: () => "" },
|
|
761
|
+
rerender: touch,
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
if (a.contextBudget?.length) {
|
|
765
|
+
parts.push(`<h2 class="mt-sec">Context budget <span>sessions re-processing the most context · a high reuse % is a lot of re-reading</span></h2>` +
|
|
766
|
+
dataTable({
|
|
767
|
+
id: "spend-ctx",
|
|
768
|
+
columns: [
|
|
769
|
+
{ key: "title", label: "session", flex: true, get: (r) => r.title ?? r.id, cell: (r) => `<a href="#" data-s="${r.id}">${esc(r.title ?? r.id.slice(0, 8))}</a>` },
|
|
770
|
+
{ key: "reuse", label: "reuse", width: 90, num: true, get: (r) => r.reuse, cell: (r) => `<span class="${r.reuse > 0.9 ? "br" : "dim"}">${(r.reuse * 100).toFixed(0)}%</span>` },
|
|
771
|
+
{ key: "cacheRead", label: "context re-read", width: 120, num: true, get: (r) => r.cacheRead, cell: (r) => tok(r.cacheRead) },
|
|
772
|
+
{ key: "cost", label: "cost", width: 88, num: true, get: (r) => r.cost, cell: (r) => usd(r.cost) },
|
|
773
|
+
{ key: "turns", label: "turns", width: 64, num: true, get: (r) => r.turns, cell: (r) => String(r.turns) },
|
|
774
|
+
],
|
|
775
|
+
rows: a.contextBudget,
|
|
776
|
+
leading: { width: 20, cell: () => "" },
|
|
777
|
+
trailing: { width: 8, cell: () => "" },
|
|
778
|
+
rerender: touch,
|
|
779
|
+
}));
|
|
780
|
+
}
|
|
781
|
+
return parts.join("");
|
|
782
|
+
}
|
|
783
|
+
|
|
670
784
|
// ---------- stats
|
|
671
785
|
// Heavier than the 5s snapshot, so it has its own endpoint: fetched when the view opens (per project
|
|
672
786
|
// scope), then refreshed at most every 30s while the view stays open.
|
|
@@ -685,6 +799,34 @@ const big = (n) => (n >= 1e9 ? `${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `${(n / 1
|
|
|
685
799
|
const toolName = (t) => String(t).replace(/^mcp__([^_]+(?:_[^_]+)*)__/, "$1 · ").replace(/^plugin_/, "");
|
|
686
800
|
const pct = (a, b) => (b ? `${((100 * a) / b).toFixed(0)}%` : "—");
|
|
687
801
|
const dur = (ms) => (ms < 3600e3 ? `${Math.round(ms / 60e3)}m` : ms < 86400e3 ? `${(ms / 3600e3).toFixed(1)}h` : `${(ms / 86400e3).toFixed(1)}d`);
|
|
802
|
+
// ---------- search view (M4.5): memory over Swarm's own data — handoffs, incidents, gates, what sessions said
|
|
803
|
+
const srch = { q: "", kind: "", hits: null, t: 0 };
|
|
804
|
+
function renderSearch() {
|
|
805
|
+
const chip = (k, label) => `<span class="chip ${srch.kind === k ? "on" : ""}" data-skind="${k}">${label}</span>`;
|
|
806
|
+
const mark = (s) => esc(s).replace(/\u0001/g, "<mark>").replace(/\u0002/g, "</mark>");
|
|
807
|
+
const link = (h) => h.kind === "session" ? `data-s="${esc(h.ref)}"` : h.sessionId ? `data-s="${esc(h.sessionId)}"` : "";
|
|
808
|
+
const hits = (srch.hits ?? []).map((h) => `<div class="hit"><div class="ht"><span class="badge">${esc(h.kind)}</span><b>${esc(h.title)}</b>${h.task ? `<span class="br">${esc(h.task)}</span>` : ""}<span class="grow"></span>${!state.sel ? `<span class="dim">${esc(projName(h.projectId))} · </span>` : ""}<span class="dim">${ago(h.ts)}</span>${link(h) ? `<a href="#" ${link(h)} title="Open the session">${ic("arrow-right", 12)}</a>` : ""}</div><div class="hs">${mark(h.snippet)}</div></div>`).join("");
|
|
809
|
+
const had = document.activeElement?.id === "srchQ" ? { pos: document.activeElement.selectionStart } : null;
|
|
810
|
+
$("#main").innerHTML =
|
|
811
|
+
`<h2>Search <span>Swarm's own memory${state.sel ? ` · ${esc(projName(state.sel))}` : " · all projects"} — handoffs, incidents, gates, what sessions said. Never your code.</span></h2>` +
|
|
812
|
+
`<div class="srch"><input id="srchQ" type="search" placeholder="pkill, login form, kind:incident git reset, task:M1.2 …" value="${esc(srch.q)}" autocomplete="off"></div>` +
|
|
813
|
+
`<div class="chips">${chip("", "All")}${chip("handoff", "Handoffs")}${chip("incident", "Incidents")}${chip("gate", "Gates")}${chip("session", "Sessions")}</div>` +
|
|
814
|
+
(srch.hits === null ? `<div class="empty">${PX.idle()}Type to search. Words are AND-ed, the last one is a prefix; quote a phrase; <code>kind:</code> and <code>task:</code> filter.</div>`
|
|
815
|
+
: hits || `<div class="empty">${PX.idle()}Nothing in memory matches <b>${esc(srch.q)}</b>.</div>`);
|
|
816
|
+
if (had) { const i = $("#srchQ"); i.focus(); i.setSelectionRange(had.pos, had.pos); }
|
|
817
|
+
}
|
|
818
|
+
async function runSearch() {
|
|
819
|
+
if (!srch.q.trim()) { srch.hits = null; return renderSearch(); }
|
|
820
|
+
const q = new URLSearchParams({ q: srch.q, limit: "50" });
|
|
821
|
+
if (state.sel) q.set("project", state.sel);
|
|
822
|
+
if (srch.kind) q.set("kind", srch.kind);
|
|
823
|
+
const mine = ++srch.t;
|
|
824
|
+
const j = await fetch(`/v1/memory?${q}`).then((r) => r.json()).catch(() => ({ hits: [] }));
|
|
825
|
+
if (mine !== srch.t) return;
|
|
826
|
+
srch.hits = j.hits ?? [];
|
|
827
|
+
if (state.view === "search" && !state.session) renderSearch();
|
|
828
|
+
}
|
|
829
|
+
document.addEventListener("input", (ev) => { if (ev.target.id === "srchQ") { srch.q = ev.target.value; clearTimeout(srch.db); srch.db = setTimeout(runSearch, 150); } });
|
|
688
830
|
function renderStats() {
|
|
689
831
|
const st = statsCache.key === (state.sel ?? "") ? statsCache.data : null;
|
|
690
832
|
const scope = state.sel ? esc(projName(state.sel)) : "all projects";
|
|
@@ -852,6 +994,55 @@ function sessionStream() {
|
|
|
852
994
|
}
|
|
853
995
|
// True when `rows` only extends the rows already in #log (same session, same prefix) → append, don't rebuild.
|
|
854
996
|
const isAppend = (rows) => logRendered && rows.length >= logRendered.length && logRendered.every((k, n) => rows[n].key === k);
|
|
997
|
+
// M4.1 session replay: step through a session's tool calls, one at a time, with full input/output
|
|
998
|
+
// (lazy-fetched from /v1/events/:seq). replayState holds the current step; nav by buttons or ←/→.
|
|
999
|
+
const replay = { steps: [], i: 0, cache: new Map() };
|
|
1000
|
+
// M4.4: resume where this died — the daemon builds the prompt from the handoff + tail; we just confirm.
|
|
1001
|
+
async function resumeDead() {
|
|
1002
|
+
const id = state.session; if (!id) return;
|
|
1003
|
+
const plan = await fetch(`/v1/sessions/${encodeURIComponent(id)}/resume`).then((r) => r.json());
|
|
1004
|
+
if (!plan.ok) return alert(plan.error);
|
|
1005
|
+
if (!confirm(`Resume ${plan.task}${plan.owner ? ` as ${plan.owner}` : ""}?\n\n${plan.prompt.slice(0, 900)}${plan.prompt.length > 900 ? "…" : ""}`)) return;
|
|
1006
|
+
const r = await fetch(`/v1/sessions/${encodeURIComponent(id)}/resume`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }).then((x) => x.json());
|
|
1007
|
+
if (!r.ok) return alert(r.error);
|
|
1008
|
+
openSession(r.run.sessionId);
|
|
1009
|
+
}
|
|
1010
|
+
function openReplay() {
|
|
1011
|
+
replay.steps = state.log.filter((e) => e.type === "tool.requested").map((e) => ({ seq: e.seq, tool: e.payload?.tool ?? "tool", summary: e.payload?.summary ?? "" }));
|
|
1012
|
+
replay.i = 0;
|
|
1013
|
+
replay.cache.clear();
|
|
1014
|
+
if (!replay.steps.length) { alert("No tool calls in this session yet."); return; }
|
|
1015
|
+
renderReplay();
|
|
1016
|
+
}
|
|
1017
|
+
async function renderReplay() {
|
|
1018
|
+
const n = replay.steps.length;
|
|
1019
|
+
const step = replay.steps[replay.i];
|
|
1020
|
+
let detail = replay.cache.get(step.seq);
|
|
1021
|
+
if (!detail) {
|
|
1022
|
+
// the request event (full input) and the paired completed event (output), both by seq
|
|
1023
|
+
const req = await fetch(`/v1/events/${step.seq}`).then((r) => r.json()).catch(() => null);
|
|
1024
|
+
const done = state.log.find((e) => e.type === "tool.completed" && e.seq > step.seq && e.payload?.summary === step.summary);
|
|
1025
|
+
const res = done ? await fetch(`/v1/events/${done.seq}`).then((r) => r.json()).catch(() => null) : null;
|
|
1026
|
+
detail = { input: req?.payload?.toolInput ?? null, output: res?.payload?.toolResponse ?? null, ts: req?.ts };
|
|
1027
|
+
replay.cache.set(step.seq, detail);
|
|
1028
|
+
}
|
|
1029
|
+
const j = (v) => (v == null ? "" : typeof v === "string" ? v : JSON.stringify(v, null, 2));
|
|
1030
|
+
$("#picker").innerHTML = `<div class="pk wn rp" role="dialog" aria-modal="true">
|
|
1031
|
+
<div class="pk-h">${ic("play", 15)}<b>Replay</b><span class="dim" style="margin-left:8px">${step.tool}</span><span class="grow"></span><span class="dim" style="font-size:var(--fs-sm)">${replay.i + 1} / ${n}${detail.ts ? ` · ${hhmm(detail.ts)}` : ""}</span><button id="pkCancel" title="Close">${ic("x", 14)}</button></div>
|
|
1032
|
+
<div class="pk-b">
|
|
1033
|
+
<div class="dim now" style="font-family:var(--mono);font-size:var(--fs-sm);margin-bottom:8px">${esc(step.summary)}</div>
|
|
1034
|
+
<h4>input</h4><pre class="snip">${esc(j(detail.input)) || '<span class="dim">—</span>'}</pre>
|
|
1035
|
+
<h4>output</h4><pre class="snip">${detail.output != null ? esc(j(detail.output)).slice(0, 4000) : '<span class="dim">(no result captured)</span>'}</pre>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div class="pk-f"><button id="rpPrev" ${replay.i === 0 ? "disabled" : ""}>${ic("arrow-left", 12)} Prev</button><span class="grow"></span><input id="rpRange" type="range" min="0" max="${n - 1}" value="${replay.i}" style="flex:1;max-width:280px"><span class="grow"></span><button class="primary" id="rpNext" ${replay.i >= n - 1 ? "disabled" : ""}>Next ${ic("arrow-right", 12)}</button></div>
|
|
1038
|
+
</div>`;
|
|
1039
|
+
}
|
|
1040
|
+
function replayGo(delta) {
|
|
1041
|
+
const n = replay.steps.length;
|
|
1042
|
+
replay.i = Math.max(0, Math.min(n - 1, replay.i + delta));
|
|
1043
|
+
renderReplay();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
855
1046
|
// Spawned sessions get a stdin box while their run is live (M3.3); interactive ones are told where to type.
|
|
856
1047
|
function stdinBox(s) {
|
|
857
1048
|
if (s.kind !== "spawned") return "";
|
|
@@ -894,7 +1085,7 @@ function renderSession() {
|
|
|
894
1085
|
const subTurns = state.turns.filter((x) => x.sidechain || x.agentId);
|
|
895
1086
|
const STAT_ICON = { cost: "coin", model: "robot", turns: "arrows-clockwise", "tool calls": "wrench", output: "chart-bar", context: "rows", started: "clock", "last seen": "eye", "subagent turns": "tree-structure" };
|
|
896
1087
|
const stat = (k, v) => `<div class="stat"><span>${ic(STAT_ICON[k] ?? "list-bullets", 13)}${k}</span><b>${v}</b></div>`;
|
|
897
|
-
const head = `<h2 class="hrow"><a class="back" href="#" id="back">${ic("arrow-left", 13)}back</a> ${esc(projName(s.projectId))} · <span class="s ${s.state}"></span> ${kindIcon(s)}${agentBadge(s.agent)}<b>${esc(s.title ?? s.id.slice(0, 8))}</b> <span>${esc(short(s.cwd))}${s.branch ? ` · ${esc(s.branch)}` : ""} · ${s.state}</span
|
|
1088
|
+
const head = `<h2 class="hrow"><a class="back" href="#" id="back">${ic("arrow-left", 13)}back</a> ${esc(projName(s.projectId))} · <span class="s ${s.state}"></span> ${kindIcon(s)}${agentBadge(s.agent)}<b>${esc(s.title ?? s.id.slice(0, 8))}</b> <span>${esc(short(s.cwd))}${s.branch ? ` · ${esc(s.branch)}` : ""} · ${s.state}</span><a href="#" class="nav" id="replay" style="margin-left:auto" title="Step through this session's tool calls">${ic("play", 13)} Replay</a>${s.state === "ended" ? `<a href="#" class="nav" id="resumeDead" title="Spawn a run that picks up this session's task from its handoff + last actions">${ic("reload", 13)} Resume where it died</a>` : ""}</h2>`;
|
|
898
1089
|
const side = `<div class="stats">
|
|
899
1090
|
${stat("cost", usd(s.costUsd))}${stat("model", esc(model(s.model)) || "—")}${stat("turns", s.turns)}${stat("tool calls", s.toolCalls)}
|
|
900
1091
|
${stat("output", `${tok(t.output)}${t.thinking ? `<small> · ${tok(t.thinking)} thinking</small>` : ""}`)}${stat("context", `${tok(ctx)}<small> · ${ctx ? ((100 * t.cacheRead) / ctx).toFixed(0) : 0}% cached</small>`)}
|
|
@@ -969,12 +1160,80 @@ function menuSpec(kind, d) {
|
|
|
969
1160
|
{ label: "Refresh pricing", icon: "arrows-clockwise", caption: "LiteLLM", run: async () => { const r = await fetch("/v1/pricing/refresh", { method: "POST" }); if (!r.ok) console.warn("pricing refresh failed", r.status); refresh(); } },
|
|
970
1161
|
{ label: "Copy dashboard URL", icon: "copy", run: () => copy(location.origin) },
|
|
971
1162
|
{ divider: true },
|
|
1163
|
+
{ label: "Desktop notifications", icon: "bell", pressed: notifyOn(), caption: notifyOn() ? "on" : "permission prompts, orphans", run: () => { notifyOn() ? disableNotifications() : enableNotifications(); $("#settings").blur(); } },
|
|
1164
|
+
{ label: "What's New", icon: "star", caption: `v${state.version ?? "?"}`, run: () => whatsNew() },
|
|
972
1165
|
{ label: "Documentation", icon: "book-open", caption: "getswarm", run: () => window.open("https://getswarm.vercel.app/docs/", "_blank") },
|
|
973
1166
|
{ label: "Send feedback", icon: "comment-text", caption: "GitHub issue", run: () => window.open(feedbackUrl(), "_blank") },
|
|
974
1167
|
] };
|
|
975
1168
|
}
|
|
976
1169
|
return null;
|
|
977
1170
|
}
|
|
1171
|
+
// M4.7 desktop notifications: native notifications (web Notification API — works in the browser and
|
|
1172
|
+
// the desktop app's webview) for the things you'd want to walk away and be pinged about — a spawned
|
|
1173
|
+
// run waiting on a permission, and a claim orphaned with unfinished work. Clicking opens the spot to
|
|
1174
|
+
// act. Off until enabled from the settings menu (which requests OS permission). Quiet while focused.
|
|
1175
|
+
const NOTIFY_KEY = "swarm.notify";
|
|
1176
|
+
const notifyOn = () => { try { return localStorage.getItem(NOTIFY_KEY) === "on"; } catch { return false; } };
|
|
1177
|
+
async function enableNotifications() {
|
|
1178
|
+
if (!("Notification" in window)) { alert("This browser doesn't support notifications."); return; }
|
|
1179
|
+
const perm = Notification.permission === "granted" ? "granted" : await Notification.requestPermission();
|
|
1180
|
+
if (perm !== "granted") { alert("Notifications were blocked. Allow them for this site in your browser/OS settings."); return; }
|
|
1181
|
+
try { localStorage.setItem(NOTIFY_KEY, "on"); } catch {}
|
|
1182
|
+
new Notification("Swarm notifications on", { body: "You'll be pinged when a run needs a permission or a claim is orphaned." });
|
|
1183
|
+
}
|
|
1184
|
+
function disableNotifications() { try { localStorage.setItem(NOTIFY_KEY, "off"); } catch {} }
|
|
1185
|
+
let lastNotifyAt = 0;
|
|
1186
|
+
function notifyForEvent(ev) {
|
|
1187
|
+
if (!notifyOn() || !("Notification" in window) || Notification.permission !== "granted") return;
|
|
1188
|
+
if (!document.hidden && ev.type !== "permission.requested") return; // only permission prompts interrupt while you're looking
|
|
1189
|
+
const now = Date.now();
|
|
1190
|
+
if (now - lastNotifyAt < 1500) return; // don't stack
|
|
1191
|
+
const p = ev.payload || {};
|
|
1192
|
+
let title, body, onClick;
|
|
1193
|
+
if (ev.type === "permission.requested") {
|
|
1194
|
+
title = `Permission needed: ${p.tool ?? "tool"}`;
|
|
1195
|
+
body = `${p.display ?? ""}
|
|
1196
|
+
${p.reason ?? ""}`.slice(0, 180);
|
|
1197
|
+
onClick = () => { if (ev.sessionId) openSession(ev.sessionId); };
|
|
1198
|
+
} else if (ev.type === "claim.orphaned") {
|
|
1199
|
+
title = "Claim orphaned";
|
|
1200
|
+
body = `${p.task ?? "a task"} — its lease expired with unfinished work in the worktree.`;
|
|
1201
|
+
onClick = () => { state.view = "board"; state.sel = ev.projectId || state.sel; state.session = null; refresh(); };
|
|
1202
|
+
} else return;
|
|
1203
|
+
lastNotifyAt = now;
|
|
1204
|
+
const n = new Notification(title, { body, tag: `swarm-${ev.type}-${ev.sessionId ?? ev.seq}` });
|
|
1205
|
+
n.onclick = () => { window.focus(); onClick?.(); n.close(); };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// What's New: release notes for the running version, from window.RELEASE_NOTES (release-notes.js).
|
|
1209
|
+
// The desktop menu calls window.swarmWhatsNew; the settings menu calls whatsNew(); it also opens
|
|
1210
|
+
// itself once after an upgrade (localStorage remembers the last version the user saw).
|
|
1211
|
+
function releaseNotesFor(version) {
|
|
1212
|
+
const all = window.RELEASE_NOTES || {};
|
|
1213
|
+
if (version && all[version]) return { version, ...all[version] };
|
|
1214
|
+
const latest = Object.keys(all)[0];
|
|
1215
|
+
return latest ? { version: latest, ...all[latest] } : null;
|
|
1216
|
+
}
|
|
1217
|
+
function whatsNew(version) {
|
|
1218
|
+
const n = releaseNotesFor(version || state.version);
|
|
1219
|
+
if (!n) return;
|
|
1220
|
+
try { localStorage.setItem("swarm.seenVersion", n.version); } catch {}
|
|
1221
|
+
$("#picker").innerHTML = `<div class="pk wn" role="dialog" aria-modal="true">
|
|
1222
|
+
<div class="pk-h">${ic("star", 15)}<b>What's New</b><span class="grow"></span><button id="pkCancel" title="Close">${ic("x", 14)}</button></div>
|
|
1223
|
+
<div class="pk-b"><h3>Swarm ${esc(n.version)}</h3>${n.date ? `<div class="date">${esc(n.date)}</div>` : ""}${n.html}</div>
|
|
1224
|
+
<div class="pk-f"><span class="grow"></span><a href="https://getswarm.vercel.app/changelog" target="_blank" rel="noopener" style="align-self:center;color:var(--dim);font-size:var(--fs-sm)">Full changelog →</a><button id="pkCancel">Close</button></div>
|
|
1225
|
+
</div>`;
|
|
1226
|
+
}
|
|
1227
|
+
window.swarmWhatsNew = (v) => whatsNew(v);
|
|
1228
|
+
// auto-open once per version, but never on the very first run (nothing to compare against)
|
|
1229
|
+
function maybeWhatsNew() {
|
|
1230
|
+
if (!state.version || !window.RELEASE_NOTES) return;
|
|
1231
|
+
let seen; try { seen = localStorage.getItem("swarm.seenVersion"); } catch {}
|
|
1232
|
+
if (seen === state.version) return;
|
|
1233
|
+
if (!seen) { try { localStorage.setItem("swarm.seenVersion", state.version); } catch {} return; }
|
|
1234
|
+
if (releaseNotesFor(state.version)) whatsNew(state.version);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
978
1237
|
// Star nudge: once a month at most, never on first open, dismissable for good. Pure localStorage —
|
|
979
1238
|
// nothing leaves the machine; clicking Star just opens the repo in a browser.
|
|
980
1239
|
const STAR = { key: "swarm.star", firstAfterMs: 2 * 86_400_000, everyMs: 30 * 86_400_000 };
|
|
@@ -1050,6 +1309,10 @@ document.addEventListener("click", async (ev) => {
|
|
|
1050
1309
|
if (!r.ok) alert(r.error); else state.tasks = null;
|
|
1051
1310
|
return refresh();
|
|
1052
1311
|
}
|
|
1312
|
+
if (t.dataset.codify) { ev.preventDefault(); return codifyIncident(t.dataset.codify); }
|
|
1313
|
+
if (t.id === "dryrun") { ev.preventDefault(); return openDryRun(); }
|
|
1314
|
+
if (t.dataset.skind !== undefined) { ev.preventDefault(); srch.kind = t.dataset.skind; return runSearch().then(renderSearch); }
|
|
1315
|
+
if (t.id === "drRun") { ev.preventDefault(); return runDryRun(); }
|
|
1053
1316
|
if (t.dataset.inc) { state.incFilter = t.dataset.inc; state.allIncidents = null; return refresh(); }
|
|
1054
1317
|
if (t.dataset.ack) {
|
|
1055
1318
|
ev.preventDefault(); ev.stopPropagation();
|
|
@@ -1094,6 +1357,8 @@ document.addEventListener("click", async (ev) => {
|
|
|
1094
1357
|
return fetch(`/v1/resources/${encodeURIComponent(t.dataset.resrelease)}?${q}`, { method: "DELETE" }).then(refresh);
|
|
1095
1358
|
}
|
|
1096
1359
|
if (t.id === "back") { ev.preventDefault(); state.session = null; return touch(); }
|
|
1360
|
+
if (t.id === "replay") { ev.preventDefault(); return openReplay(); }
|
|
1361
|
+
if (t.id === "resumeDead") { ev.preventDefault(); return resumeDead(); }
|
|
1097
1362
|
if (t.dataset.s) { ev.preventDefault(); return openSession(t.dataset.s); }
|
|
1098
1363
|
if (t.dataset.id !== undefined) { state.sel = t.dataset.id || null; localStorage.setItem("swarm.sel", state.sel ?? ""); state.session = null; state.tasks = null; state.dirty = true; return refresh(); }
|
|
1099
1364
|
});
|
|
@@ -1194,13 +1459,21 @@ $("#picker").addEventListener("click", (ev) => {
|
|
|
1194
1459
|
if (ev.target.id === "picker" || ev.target.closest("#pkCancel")) return closePicker();
|
|
1195
1460
|
const go = ev.target.closest("[data-go]");
|
|
1196
1461
|
if (go) return void pickerGo(go.dataset.go);
|
|
1462
|
+
const ctoml = ev.target.closest("[data-copy-toml]"), cles = ev.target.closest("[data-copy-lesson]");
|
|
1463
|
+
if (ctoml) { ev.preventDefault(); copy($(`#toml-${ctoml.dataset.copyToml}`)?.textContent); ctoml.lastChild.textContent = " copied"; return; }
|
|
1464
|
+
if (cles) { ev.preventDefault(); copy($(`#lesson-${cles.dataset.copyLesson}`)?.textContent); cles.lastChild.textContent = " copied"; return; }
|
|
1465
|
+
if (ev.target.closest("#rpPrev")) return replayGo(-1);
|
|
1466
|
+
if (ev.target.closest("#rpNext")) return replayGo(1);
|
|
1197
1467
|
if (ev.target.closest("#rnCancel")) return closePicker();
|
|
1198
1468
|
const rnGo = ev.target.closest("#rnGo"); if (rnGo) return submitRun(rnGo.dataset.task);
|
|
1199
1469
|
if (ev.target.closest("#pkAdd")) { const p = $("#pkPath")?.value.trim() || picker.path; closePicker(); addProject(p); }
|
|
1200
1470
|
});
|
|
1471
|
+
$("#picker").addEventListener("input", (ev) => { if (ev.target.id === "rpRange") { replay.i = Number(ev.target.value); renderReplay(); } });
|
|
1472
|
+
$("#picker").addEventListener("change", (ev) => { if (ev.target.dataset?.drmode) { dry.modes[ev.target.dataset.drmode] = ev.target.value; } });
|
|
1201
1473
|
$("#picker").addEventListener("keydown", (ev) => {
|
|
1202
1474
|
if (ev.key === "Enter" && ev.target.id === "pkPath") { ev.preventDefault(); pickerGo(ev.target.value.trim()); }
|
|
1203
1475
|
if (ev.key === "Enter" && (ev.metaKey || ev.ctrlKey) && ev.target.id === "rnPrompt") { ev.preventDefault(); submitRun($("#rnGo")?.dataset.task); }
|
|
1476
|
+
if ((ev.key === "ArrowRight" || ev.key === "ArrowLeft") && $(".rp")) { ev.preventDefault(); replayGo(ev.key === "ArrowRight" ? 1 : -1); }
|
|
1204
1477
|
});
|
|
1205
1478
|
document.addEventListener("keydown", (ev) => { if (ev.key === "Escape" && $("#picker").innerHTML) closePicker(); });
|
|
1206
1479
|
|
|
@@ -1228,6 +1501,7 @@ function connect() {
|
|
|
1228
1501
|
if (state.log.length > LOG_CAP) state.log.shift();
|
|
1229
1502
|
schedule();
|
|
1230
1503
|
}
|
|
1504
|
+
if (fresh) notifyForEvent(ev);
|
|
1231
1505
|
pollSoon();
|
|
1232
1506
|
};
|
|
1233
1507
|
for (const t of ["session.started", "session.ended", "prompt.submitted", "tool.requested", "tool.completed", "subagent.started", "subagent.stopped", "agent.text", "session.notification", "incident.opened", "claim.acquired", "claim.released", "resource.acquired", "resource.released", "resource.reaped", "process.started", "process.exited", "gate.recorded", "claim.orphaned", "claim.renewed", "permission.requested", "permission.resolved"]) es.addEventListener(t, onAny);
|
package/web/icons.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// generated by tools/build.ts — pixelarticons (MIT), 24x24 grid
|
|
2
|
-
const ICON_PATHS = {"squares-four":"<path d=\"M4 2h16v2H4zM2 4h2v16H2zm2 7h16v2H4zm16-7h2v16h-2z\"/>\n <path d=\"M11 4h2v18h-2z\"/>\n <path d=\"M4 20h16v2H4z\"/>","timer":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","coins":"<path d=\"M6 2h6v2H6zM4 4h2v2H4zm8 0h2v2h-2zm-8 8h2v2H4zm8 0h2v2h-2zm-6 2h6v2H6zM2 6h2v6H2zm12 0h2v6h-2z\"/>\n <path d=\"M14 8h4v2h-4zm-4 10h2v2h-2zm8-8h2v2h-2zm-6 10h2v2h-2zm6-2h2v2h-2z\"/>\n <path d=\"M12 20h6v2h-6zm-4-6h2v4H8zm12-2h2v6h-2zM7 6h4v2H7z\"/>\n <path d=\"M9 6h2v6H9zm6 8h2v4h-2zm-1-2h3v2h-3z\"/>","pulse":"<path d=\"M22 22H4v-2h18v2ZM4 20H2V2h2v18Zm4-6H6v-2h2v2Zm8 0h-2v-2h2v2Zm-6-2H8v-2h2v2Zm4 0h-2v-2h2v2Zm4 0h-2v-2h2v2Zm-6-2h-2V8h2v2Zm8 0h-2V8h2v2Zm2-2h-2V6h2v2Z\"/>","folder-simple":"<path d=\"M4 4h6v2H4zm0 14h16v2H4zM20 8h2v10h-2zM2 6h2v12H2zm8 0h10v2H10z\"/>","folders":"<path d=\"M9 3H7v14h2zM5 7H3v14h2zm12-6H9v2h8zm4 4h-2v12h2zm-2 12H9v2h10zm-4 4H5v2h10zm2-18h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M13 7h6v2h-6zM5 5h2v2H5zm10 14h2v2h-2z\"/>","push-pin":"<path d=\"M7 2h10v2H7zM5 4h2v2H5zm14 0h-2v2h2zM7 17h2v2H7zm2 2h2v2H9zm6-2h2v2h-2zm-2 2h2v2h-2zm-2 2h2v2h-2zm-6-7h2v3H5zm12 0h2v3h-2zM3 6h2v8H3zm18 0h-2v8h2zM10 6h4v2h-4zM8 8h2v4H8zm2 4h4v2h-4zm4-4h2v4h-2z\"/>","push-pin-slash":"<path d=\"M7 2h10v2H7zM5 4h2v2H5zm14 0h-2v2h2zM7 17h2v2H7zm2 2h2v2H9zm6-2h2v2h-2zm-2 2h2v2h-2zm-2 2h2v2h-2zm-6-7h2v3H5zm12 0h2v3h-2zM3 6h2v8H3zm18 0h-2v8h2zM10 6h4v2h-4zM8 8h2v4H8zm2 4h4v2h-4zm4-4h2v4h-2z\"/>","x":"<path d=\"M7 19H5V17H7V19ZM19 19H17V17H19V19ZM9 15V17H7V15H9ZM17 17H15V15H17V17ZM11 15H9V13H11V15ZM15 15H13V13H15V15ZM13 13H11V11H13V13ZM11 11H9V9H11V11ZM15 11H13V9H15V11ZM9 9H7V7H9V9ZM17 9H15V7H17V9ZM7 7H5V5H7V7ZM19 7H17V5H19V7Z\"/>","plus":"<path d=\"M13 11h7v2h-7v7h-2v-7H4v-2h7V4h2v7Z\"/>","dots-three":"<path d=\"M3 9h2v2H3zm8 0h2v2h-2zm8 0h2v2h-2zM1 11h2v2H1zm8 0h2v2H9zm8 0h2v2h-2zM3 13h2v2H3zm8 0h2v2h-2zm8 0h2v2h-2zM5 11h2v2H5zm8 0h2v2h-2zm8 0h2v2h-2z\"/>","dots-three-vertical":"<path d=\"M15 3v2h-2V3zm0 8v2h-2v-2zm0 8v2h-2v-2zM13 1v2h-2V1zm0 8v2h-2V9zm0 8v2h-2v-2zM11 3v2H9V3zm0 8v2H9v-2zm0 8v2H9v-2zm2-14v2h-2V5zm0 8v2h-2v-2zm0 8v2h-2v-2z\"/>","keyboard":"<path d=\"M21 21H3v-2h18v2ZM3 19H1V5h2v14Zm20 0h-2V5h2v14Zm-5-2H6v-2h12v2Zm-9-4H7v-2h2v2Zm4 0h-2v-2h2v2Zm4 0h-2v-2h2v2ZM7 9H5V7h2v2Zm4 0H9V7h2v2Zm4 0h-2V7h2v2Zm4 0h-2V7h2v2Zm2-4H3V3h18v2Z\"/>","play":"<path d=\"M15 11h-2V9h2zm0 4h-2v-2h2zm-2 2h-2v-2h2zm0-8h-2V7h2zm-2-2H9V5h2zM9 21H7V3h2zm6-8h2v-2h-2zm-6 4h2v2H9z\"/>","tree-structure":"<path d=\"M6 4h2v2H6zm2-2h8v2H8zm10 4h2v4h-2zm2 4h2v6h-2zm-2 6h2v2h-2zM4 16h2v2H4zm-2-6h2v6H2zm4 8h12v2H6zM4 6h2v4H4z\"/>\n <path d=\"M11 18h2v4h-2zm5-14h2v2h-2z\"/>","robot":"<path d=\"M5 7h14v2H5zm0 12h14v2H5zM3 9h2v10H3zm16 0h2v10h-2zM1 13h2v2H1zm20 0h2v2h-2zM11 5h2v2h-2zM7 3h4v2H7zm1 9h2v4H8zm6 0h2v4h-2z\"/>","coin":"<path d=\"M6 2h6v2H6zM4 4h2v2H4zm8 0h2v2h-2zm-8 8h2v2H4zm8 0h2v2h-2zm-6 2h6v2H6zM2 6h2v6H2zm12 0h2v6h-2z\"/>\n <path d=\"M14 8h4v2h-4zm-4 10h2v2h-2zm8-8h2v2h-2zm-6 10h2v2h-2zm6-2h2v2h-2z\"/>\n <path d=\"M12 20h6v2h-6zm-4-6h2v4H8zm12-2h2v6h-2zM7 6h4v2H7z\"/>\n <path d=\"M9 6h2v6H9zm6 8h2v4h-2zm-1-2h3v2h-3z\"/>","cpu":"<path d=\"M5 3h14v2H5zm0 16h14v2H5zM3 5h2v14H3zm16 0h2v14h-2zM9 7h6v2H9zm0 8h6v2H9zM7 9h2v6H7zm8 0h2v6h-2zm-4-8h2v2h-2zm0 20h2v2h-2zM1 11h2v2H1zm20 0h2v2h-2zm0-4h2v2h-2zm0 8h2v2h-2zM1 15h2v2H1zm0-8h2v2H1zm6-6h2v2H7zm8 0h2v2h-2zm0 20h2v2h-2zm-8 0h2v2H7z\"/>","arrows-clockwise":"<path d=\"M13 22H11V15H13V22ZM5 21H3V19H5V21ZM21 19V21H19V19H21ZM7 19H5V17H7V19ZM19 19H17V17H19V19ZM9 17H7V15H9V17ZM17 17H15V15H17V17ZM9 13H2V11H9V13ZM22 13H15V11H22V13ZM9 9H7V7H9V9ZM13 9H11V2H13V9ZM17 9H15V7H17V9ZM7 7H5V5H7V7ZM19 7H17V5H19V7ZM5 5H3V3H5V5ZM21 5H19V3H21V5Z\"/>","lightning":"<path d=\"M4 20h16v2H4zM4 2h16v2H4zM2 4h2v16H2zm18 0h2v16h-2zM9 15h6v2H9zM7 9h2v6H7zm8 0h2v6h-2zm-4-2h2v5h-2z\"/>","clock":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","wrench":"<path d=\"M18 22H13V24H11V22H6V20H18V22ZM4 22H2V20H4V22ZM22 22H20V20H22V22ZM6 20H4V18H6V20ZM20 20H18V18H20V20ZM4 18H2V13H0V11H2V6H4V18ZM8 18H6V16H8V18ZM22 11H24V13H22V18H20V13H16V16H8V8H16V11H20V6H22V11ZM10 10V14H14V10H10ZM8 8H6V6H8V8ZM6 6H4V4H6V6ZM20 6H18V4H20V6ZM4 4H2V2H4V4ZM13 2H18V4H6V2H11V0H13V2ZM22 4H20V2H22V4Z\"/>","file-text":"<path d=\"M6 4H4v16h2zm10-2H6v2h10zm4 4h-2v14h2zm-2 14H6v2h12zM16 4h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M12 8h6v2h-6zm-4 8h8v2H8zm0-4h8v2H8zm0-4h2v2H8z\"/>","git-branch":"<path d=\"M2 14h8v2H2zm0 6h8v2H2zm0-4h2v4H2zm6 0h2v4H8zm6-14h8v2h-8zm0 6h8v2h-8zm0-4h2v4h-2zm6 0h2v4h-2zm-8 13h7v2h-7zm5-5h2v5h-2zM5 2h2v10H5z\"/>","git-commit":"<path d=\"M7 7h10v2H7zm0 2h2v6H7zm0 6h10v2H7zm8-6h2v6h-2zM0 11h5v2H0zm19 0h5v2h-5z\"/>","arrow-left":"<path d=\"M20 11v2H4v-2zM8 13v2H6v-2zm2 2v2H8v-2zm2 2v2h-2v-2zm-4-6V9H6v2z\"/>\n <path d=\"M10 15V7H8v8zm2 2V5h-2v12z\"/>","warning":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM20 4h2v16h-2zM2 4h2v16H2zm9 2h2v8h-2zm0 10h2v2h-2z\"/>","check":"<path d=\"M10 18H8v-2h2v2Zm-2-2H6v-2h2v2Zm4-2v2h-2v-2h2Zm-6 0H4v-2h2v2Zm8 0h-2v-2h2v2Zm2-2h-2v-2h2v2Zm2-2h-2V8h2v2Zm2-2h-2V6h2v2Z\"/>","copy":"<path d=\"M8 6h12v2H8zM4 2h12v2H4zm2 6h2v12H6zM2 4h2v12H2zm6 16h12v2H8zM20 8h2v12h-2zm-4-4h2v2h-2zM4 16h2v2H4z\"/>","arrow-square-out":"<path d=\"M11 5H5v2h6V5ZM5 7H3v12h2V7Zm12 12H5v2h12v-2Zm2-6h-2v6h2v-6Zm-8 0H9v2h2v-2Zm2-2h-2v2h2v-2Zm2-2h-2v2h2V9Zm2-2h-2v2h2V7Zm2-2h-2v2h2V5Zm2-2h-2v8h2V3Z\"/>\n <path d=\"M21 3h-8v2h8V3Z\"/>","moon":"<path d=\"M18 22H8v-2h10v2ZM8 20H6v-2h2v2Zm12 0h-2v-2h2v2ZM6 18H4v-2h2v2Zm16 0h-2v-4h-2v-2h2v-2h2v8ZM4 16H2V6h2v10Zm14 0h-6v-2h6v2Zm-6-2h-2v-2h2v2Zm-2-2H8V6h2v6ZM6 6H4V4h2v2Zm8-2h-2v2h-2V4H6V2h8v2Z\"/>","sun":"<path d=\"M13 22h-2v-3h2v3Zm-6-3H5v-2h2v2Zm12 0h-2v-2h2v2Zm-4-2H9v-2h6v2Zm-6-2H7V9h2v6Zm8 0h-2V9h2v6ZM5 13H2v-2h3v2Zm17 0h-3v-2h3v2Zm-7-4H9V7h6v2ZM7 7H5V5h2v2Zm12 0h-2V5h2v2Zm-6-2h-2V2h2v3Z\"/>","monitor":"<path d=\"M4 2h16v2H4zm0 14h16v2H4zM2 4h2v12H2zm18 0h2v12h-2zm-9 14h2v2h-2zm-3 2h8v2H8z\"/>","gear":"<path d=\"M18 22H13V24H11V22H6V20H18V22ZM4 22H2V20H4V22ZM22 22H20V20H22V22ZM6 20H4V18H6V20ZM20 20H18V18H20V20ZM4 18H2V13H0V11H2V6H4V18ZM8 18H6V16H8V18ZM22 11H24V13H22V18H20V13H16V16H8V8H16V11H20V6H22V11ZM10 10V14H14V10H10ZM8 8H6V6H8V8ZM6 6H4V4H6V6ZM20 6H18V4H20V6ZM4 4H2V2H4V4ZM13 2H18V4H6V2H11V0H13V2ZM22 4H20V2H22V4Z\"/>","book-open":"<path d=\"M2 3h9v2H2zM0 19h11v2H0zM13 3h9v2h-9zm0 16h11v2H13zM11 5h2v18h-2zM0 5h2v14H0zm22 0h2v14h-2zm-7 2h5v2h-5zm0 4h5v2h-5zm0 4h2v2h-2z\"/>","trash":"<path d=\"M18 22H6V20H18V22ZM9 6H15V4H17V6H22V8H20V20H18V8H6V20H4V8H2V6H7V4H9V6ZM15 4H9V2H15V4Z\"/>","magnifying-glass":"<path d=\"M22 22h-2v-2h2v2Zm-2-2h-2v-2h2v2Zm-6-2H6v-2h8v2Zm4 0h-2v-2h2v2ZM6 16H4v-2h2v2Zm10 0h-2v-2h2v2ZM4 14H2V6h2v8Zm14 0h-2V6h2v8ZM6 6H4V4h2v2Zm10 0h-2V4h2v2Zm-2-2H6V2h8v2Z\"/>","terminal-window":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM2 4h2v16H2zm18 0h2v16h-2zM6 16h2v2H6zm2-2h2v2H8zm-2-2h2v2H6z\"/>","stack":"<path d=\"M9 3H7v14h2zM5 7H3v14h2zm12-6H9v2h8zm4 4h-2v12h2zm-2 12H9v2h10zm-4 4H5v2h10zm2-18h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M13 7h6v2h-6zM5 5h2v2H5zm10 14h2v2h-2z\"/>","chart-bar":"<path d=\"M4 20h18v2H4zM2 2h2v18H2zm16 11v3h-2v-3zM8 13v3H6v-3zm8-2v2H8v-2zm0 5v2H8v-2zm4-12v3h-2V4zM8 4v3H6V4zm10-2v2H8V2zm0 5v2H8V7z\"/>","clock-counter-clockwise":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","brain":"<path d=\"M4 6h16v2H4zm0 14h16v2H4zM2 8h2v12H2zm18 0h2v12h-2z\"/>\n <path d=\"M11 4h2v4h-2zm-3 6h2v2H8zm6 0h4v2h-4zm-1-8h4v2h-4zM0 12h2v2H0zm22 0h2v2h-2zm-12 4h4v2h-4zm-2-2h2v2H8zm6 0h2v2h-2z\"/>","eye":"<path d=\"M16 20H8v-2h8v2Zm-8-2H4v-2h4v2Zm12 0h-4v-2h4v2ZM4 16H2v-2h2v2Zm10-6h-2v2h2v-2h2v4h-2v2h-4v-2H8v-4h2V8h4v2Zm8 6h-2v-2h2v2ZM2 14H0v-4h2v4Zm22 0h-2v-4h2v4ZM4 10H2V8h2v2Zm18 0h-2V8h2v2ZM8 8H4V6h4v2Zm12 0h-4V6h4v2Zm-4-2H8V4h8v2Z\"/>","pause":"<path d=\"M10 20H4V4h6v16Zm8-16v16h-6V4h6Zm-4 2v12h2V6h-2ZM6 18h2V6H6v12Z\"/>","stop":"<path d=\"M20 20H4V4H20V20ZM6 18H18V6H6V18ZM14 14H10V10H14V14Z\"/>","broadcast":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM20 4h2v16h-2zM2 4h2v16H2zm5 11h2v2H7zm0-4h4v2H7zm0-4h6v2H7zm4 6h2v4h-2zm4-2h2v6h-2zm-2-2h2v2h-2z\"/>","plugs-connected":"<path d=\"M16 18h-3v4h-2v-4H8v-2h8v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-8-9h4V2h2v5h5v2h-1v5h-2V9H6v5H4V9H3V7h5V2h2v5Z\"/>","plugs":"<path d=\"M16 18h-3v4h-2v-4H8v-2h8v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-8-9h4V2h2v5h5v2h-1v5h-2V9H6v5H4V9H3V7h5V2h2v5Z\"/>","currency-dollar":"<path d=\"M17 18h-4v4h-2v-4H7v-2h10v2Zm2-2h-2v-3h2v3Zm-2-3H7v-2h10v2ZM7 11H5V8h2v3Zm6-5h4v2H7V6h4V2h2v4Z\"/>","rows":"<path d=\"M10 5h12v2H10zm0 4h8v2h-8zm0 4h12v2H10zm0 4h8v2h-8zm-4-6H4V9h2v2ZM4 9H2V7h2v2Zm4 0H6V7h2v2ZM6 7H4V5h2v2Zm-2 6h2v2H4zm0 4h2v2H4zm-2 0v-2h2v2zm4 0v-2h2v2z\"/>","list-bullets":"<path d=\"M10 5h12v2H10zm0 4h8v2h-8zm0 4h12v2H10zm0 4h8v2h-8zm-4-6H4V9h2v2ZM4 9H2V7h2v2Zm4 0H6V7h2v2ZM6 7H4V5h2v2Zm-2 6h2v2H4zm0 4h2v2H4zm-2 0v-2h2v2zm4 0v-2h2v2z\"/>","sliders":"<path d=\"M8 14H7v6H5v-6H2v-2h6v2Zm5 6h-2V10h2v10Zm9-2h-3v2h-2v-2h-1v-2h6v2Zm-3-4h-2V4h2v10ZM7 10H5V4h2v6Zm6-4h2v2H9V6h2V4h2v2Z\"/>","comment-text":"<path d=\"M4 2h16v2H4zm0 14h14v2H4zm2-6h6v2H6zm0-4h12v2H6zM2 4h2v12H2zm18 0h2v18h-2zm-2 14h2v2h-2z\"/>","arrow-bar-left":"<path d=\"M4 4v16H2V4zm18 7v2H6v-2zm-12 2v2H8v-2zm2 2v2h-2v-2zm2 2v2h-2v-2zm-4-8v2H8V9zm2-2v2h-2V7zm2-2v2h-2V5z\"/>","arrow-bar-right":"<path d=\"M20 4v16h2V4zM2 11v2h16v-2zm12 2v2h2v-2zm-2 2v2h2v-2zm-2 2v2h2v-2zm4-8v2h2V9zm-2-2v2h2V7zm-2-2v2h2V5z\"/>","git-pull-request":"<path d=\"M2 10h8V8H2zm0-6h8V2H2zm0 4h2V4H2zm6 0h2V4H8zm6 14h8v-2h-8zm0-6h8v-2h-8zm0 4h2v-4h-2zm6 0h2v-4h-2zM12 7h5V5h-5zM5 22h2V12H5z\"/>","star":"<path d=\"M5 20H8V22H3V16H5V20ZM21 22H16V20H19V16H21V22ZM10 20H8V18H10V20ZM16 20H14V18H16V20ZM14 18H10V16H14V18ZM7 16H5V13H7V16ZM19 16H17V13H19V16ZM5 13H3V11H5V13ZM21 13H19V11H21V13ZM9 9H3V11H1V7H9V9ZM23 11H21V9H15V7H23V11ZM11 7H9V3H11V7ZM15 7H13V3H15V7ZM13 3H11V1H13V3Z\"/>","arrow-right":"<path d=\"M4 11v2h16v-2zm12 2v2h2v-2zm-2 2v2h2v-2zm-2 2v2h2v-2zm4-6V9h2v2z\"/>\n <path d=\"M14 15V7h2v8zm-2 2V5h2v12z\"/>"};
|
|
2
|
+
const ICON_PATHS = {"squares-four":"<path d=\"M4 2h16v2H4zM2 4h2v16H2zm2 7h16v2H4zm16-7h2v16h-2z\"/>\n <path d=\"M11 4h2v18h-2z\"/>\n <path d=\"M4 20h16v2H4z\"/>","timer":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","coins":"<path d=\"M6 2h6v2H6zM4 4h2v2H4zm8 0h2v2h-2zm-8 8h2v2H4zm8 0h2v2h-2zm-6 2h6v2H6zM2 6h2v6H2zm12 0h2v6h-2z\"/>\n <path d=\"M14 8h4v2h-4zm-4 10h2v2h-2zm8-8h2v2h-2zm-6 10h2v2h-2zm6-2h2v2h-2z\"/>\n <path d=\"M12 20h6v2h-6zm-4-6h2v4H8zm12-2h2v6h-2zM7 6h4v2H7z\"/>\n <path d=\"M9 6h2v6H9zm6 8h2v4h-2zm-1-2h3v2h-3z\"/>","pulse":"<path d=\"M22 22H4v-2h18v2ZM4 20H2V2h2v18Zm4-6H6v-2h2v2Zm8 0h-2v-2h2v2Zm-6-2H8v-2h2v2Zm4 0h-2v-2h2v2Zm4 0h-2v-2h2v2Zm-6-2h-2V8h2v2Zm8 0h-2V8h2v2Zm2-2h-2V6h2v2Z\"/>","folder-simple":"<path d=\"M4 4h6v2H4zm0 14h16v2H4zM20 8h2v10h-2zM2 6h2v12H2zm8 0h10v2H10z\"/>","folders":"<path d=\"M9 3H7v14h2zM5 7H3v14h2zm12-6H9v2h8zm4 4h-2v12h2zm-2 12H9v2h10zm-4 4H5v2h10zm2-18h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M13 7h6v2h-6zM5 5h2v2H5zm10 14h2v2h-2z\"/>","push-pin":"<path d=\"M7 2h10v2H7zM5 4h2v2H5zm14 0h-2v2h2zM7 17h2v2H7zm2 2h2v2H9zm6-2h2v2h-2zm-2 2h2v2h-2zm-2 2h2v2h-2zm-6-7h2v3H5zm12 0h2v3h-2zM3 6h2v8H3zm18 0h-2v8h2zM10 6h4v2h-4zM8 8h2v4H8zm2 4h4v2h-4zm4-4h2v4h-2z\"/>","push-pin-slash":"<path d=\"M7 2h10v2H7zM5 4h2v2H5zm14 0h-2v2h2zM7 17h2v2H7zm2 2h2v2H9zm6-2h2v2h-2zm-2 2h2v2h-2zm-2 2h2v2h-2zm-6-7h2v3H5zm12 0h2v3h-2zM3 6h2v8H3zm18 0h-2v8h2zM10 6h4v2h-4zM8 8h2v4H8zm2 4h4v2h-4zm4-4h2v4h-2z\"/>","x":"<path d=\"M7 19H5V17H7V19ZM19 19H17V17H19V19ZM9 15V17H7V15H9ZM17 17H15V15H17V17ZM11 15H9V13H11V15ZM15 15H13V13H15V15ZM13 13H11V11H13V13ZM11 11H9V9H11V11ZM15 11H13V9H15V11ZM9 9H7V7H9V9ZM17 9H15V7H17V9ZM7 7H5V5H7V7ZM19 7H17V5H19V7Z\"/>","plus":"<path d=\"M13 11h7v2h-7v7h-2v-7H4v-2h7V4h2v7Z\"/>","dots-three":"<path d=\"M3 9h2v2H3zm8 0h2v2h-2zm8 0h2v2h-2zM1 11h2v2H1zm8 0h2v2H9zm8 0h2v2h-2zM3 13h2v2H3zm8 0h2v2h-2zm8 0h2v2h-2zM5 11h2v2H5zm8 0h2v2h-2zm8 0h2v2h-2z\"/>","dots-three-vertical":"<path d=\"M15 3v2h-2V3zm0 8v2h-2v-2zm0 8v2h-2v-2zM13 1v2h-2V1zm0 8v2h-2V9zm0 8v2h-2v-2zM11 3v2H9V3zm0 8v2H9v-2zm0 8v2H9v-2zm2-14v2h-2V5zm0 8v2h-2v-2zm0 8v2h-2v-2z\"/>","keyboard":"<path d=\"M21 21H3v-2h18v2ZM3 19H1V5h2v14Zm20 0h-2V5h2v14Zm-5-2H6v-2h12v2Zm-9-4H7v-2h2v2Zm4 0h-2v-2h2v2Zm4 0h-2v-2h2v2ZM7 9H5V7h2v2Zm4 0H9V7h2v2Zm4 0h-2V7h2v2Zm4 0h-2V7h2v2Zm2-4H3V3h18v2Z\"/>","play":"<path d=\"M15 11h-2V9h2zm0 4h-2v-2h2zm-2 2h-2v-2h2zm0-8h-2V7h2zm-2-2H9V5h2zM9 21H7V3h2zm6-8h2v-2h-2zm-6 4h2v2H9z\"/>","tree-structure":"<path d=\"M6 4h2v2H6zm2-2h8v2H8zm10 4h2v4h-2zm2 4h2v6h-2zm-2 6h2v2h-2zM4 16h2v2H4zm-2-6h2v6H2zm4 8h12v2H6zM4 6h2v4H4z\"/>\n <path d=\"M11 18h2v4h-2zm5-14h2v2h-2z\"/>","robot":"<path d=\"M5 7h14v2H5zm0 12h14v2H5zM3 9h2v10H3zm16 0h2v10h-2zM1 13h2v2H1zm20 0h2v2h-2zM11 5h2v2h-2zM7 3h4v2H7zm1 9h2v4H8zm6 0h2v4h-2z\"/>","coin":"<path d=\"M6 2h6v2H6zM4 4h2v2H4zm8 0h2v2h-2zm-8 8h2v2H4zm8 0h2v2h-2zm-6 2h6v2H6zM2 6h2v6H2zm12 0h2v6h-2z\"/>\n <path d=\"M14 8h4v2h-4zm-4 10h2v2h-2zm8-8h2v2h-2zm-6 10h2v2h-2zm6-2h2v2h-2z\"/>\n <path d=\"M12 20h6v2h-6zm-4-6h2v4H8zm12-2h2v6h-2zM7 6h4v2H7z\"/>\n <path d=\"M9 6h2v6H9zm6 8h2v4h-2zm-1-2h3v2h-3z\"/>","cpu":"<path d=\"M5 3h14v2H5zm0 16h14v2H5zM3 5h2v14H3zm16 0h2v14h-2zM9 7h6v2H9zm0 8h6v2H9zM7 9h2v6H7zm8 0h2v6h-2zm-4-8h2v2h-2zm0 20h2v2h-2zM1 11h2v2H1zm20 0h2v2h-2zm0-4h2v2h-2zm0 8h2v2h-2zM1 15h2v2H1zm0-8h2v2H1zm6-6h2v2H7zm8 0h2v2h-2zm0 20h2v2h-2zm-8 0h2v2H7z\"/>","arrows-clockwise":"<path d=\"M13 22H11V15H13V22ZM5 21H3V19H5V21ZM21 19V21H19V19H21ZM7 19H5V17H7V19ZM19 19H17V17H19V19ZM9 17H7V15H9V17ZM17 17H15V15H17V17ZM9 13H2V11H9V13ZM22 13H15V11H22V13ZM9 9H7V7H9V9ZM13 9H11V2H13V9ZM17 9H15V7H17V9ZM7 7H5V5H7V7ZM19 7H17V5H19V7ZM5 5H3V3H5V5ZM21 5H19V3H21V5Z\"/>","lightning":"<path d=\"M4 20h16v2H4zM4 2h16v2H4zM2 4h2v16H2zm18 0h2v16h-2zM9 15h6v2H9zM7 9h2v6H7zm8 0h2v6h-2zm-4-2h2v5h-2z\"/>","clock":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","wrench":"<path d=\"M18 22H13V24H11V22H6V20H18V22ZM4 22H2V20H4V22ZM22 22H20V20H22V22ZM6 20H4V18H6V20ZM20 20H18V18H20V20ZM4 18H2V13H0V11H2V6H4V18ZM8 18H6V16H8V18ZM22 11H24V13H22V18H20V13H16V16H8V8H16V11H20V6H22V11ZM10 10V14H14V10H10ZM8 8H6V6H8V8ZM6 6H4V4H6V6ZM20 6H18V4H20V6ZM4 4H2V2H4V4ZM13 2H18V4H6V2H11V0H13V2ZM22 4H20V2H22V4Z\"/>","file-text":"<path d=\"M6 4H4v16h2zm10-2H6v2h10zm4 4h-2v14h2zm-2 14H6v2h12zM16 4h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M12 8h6v2h-6zm-4 8h8v2H8zm0-4h8v2H8zm0-4h2v2H8z\"/>","git-branch":"<path d=\"M2 14h8v2H2zm0 6h8v2H2zm0-4h2v4H2zm6 0h2v4H8zm6-14h8v2h-8zm0 6h8v2h-8zm0-4h2v4h-2zm6 0h2v4h-2zm-8 13h7v2h-7zm5-5h2v5h-2zM5 2h2v10H5z\"/>","git-commit":"<path d=\"M7 7h10v2H7zm0 2h2v6H7zm0 6h10v2H7zm8-6h2v6h-2zM0 11h5v2H0zm19 0h5v2h-5z\"/>","arrow-left":"<path d=\"M20 11v2H4v-2zM8 13v2H6v-2zm2 2v2H8v-2zm2 2v2h-2v-2zm-4-6V9H6v2z\"/>\n <path d=\"M10 15V7H8v8zm2 2V5h-2v12z\"/>","warning":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM20 4h2v16h-2zM2 4h2v16H2zm9 2h2v8h-2zm0 10h2v2h-2z\"/>","check":"<path d=\"M10 18H8v-2h2v2Zm-2-2H6v-2h2v2Zm4-2v2h-2v-2h2Zm-6 0H4v-2h2v2Zm8 0h-2v-2h2v2Zm2-2h-2v-2h2v2Zm2-2h-2V8h2v2Zm2-2h-2V6h2v2Z\"/>","copy":"<path d=\"M8 6h12v2H8zM4 2h12v2H4zm2 6h2v12H6zM2 4h2v12H2zm6 16h12v2H8zM20 8h2v12h-2zm-4-4h2v2h-2zM4 16h2v2H4z\"/>","arrow-square-out":"<path d=\"M11 5H5v2h6V5ZM5 7H3v12h2V7Zm12 12H5v2h12v-2Zm2-6h-2v6h2v-6Zm-8 0H9v2h2v-2Zm2-2h-2v2h2v-2Zm2-2h-2v2h2V9Zm2-2h-2v2h2V7Zm2-2h-2v2h2V5Zm2-2h-2v8h2V3Z\"/>\n <path d=\"M21 3h-8v2h8V3Z\"/>","moon":"<path d=\"M18 22H8v-2h10v2ZM8 20H6v-2h2v2Zm12 0h-2v-2h2v2ZM6 18H4v-2h2v2Zm16 0h-2v-4h-2v-2h2v-2h2v8ZM4 16H2V6h2v10Zm14 0h-6v-2h6v2Zm-6-2h-2v-2h2v2Zm-2-2H8V6h2v6ZM6 6H4V4h2v2Zm8-2h-2v2h-2V4H6V2h8v2Z\"/>","sun":"<path d=\"M13 22h-2v-3h2v3Zm-6-3H5v-2h2v2Zm12 0h-2v-2h2v2Zm-4-2H9v-2h6v2Zm-6-2H7V9h2v6Zm8 0h-2V9h2v6ZM5 13H2v-2h3v2Zm17 0h-3v-2h3v2Zm-7-4H9V7h6v2ZM7 7H5V5h2v2Zm12 0h-2V5h2v2Zm-6-2h-2V2h2v3Z\"/>","monitor":"<path d=\"M4 2h16v2H4zm0 14h16v2H4zM2 4h2v12H2zm18 0h2v12h-2zm-9 14h2v2h-2zm-3 2h8v2H8z\"/>","gear":"<path d=\"M18 22H13V24H11V22H6V20H18V22ZM4 22H2V20H4V22ZM22 22H20V20H22V22ZM6 20H4V18H6V20ZM20 20H18V18H20V20ZM4 18H2V13H0V11H2V6H4V18ZM8 18H6V16H8V18ZM22 11H24V13H22V18H20V13H16V16H8V8H16V11H20V6H22V11ZM10 10V14H14V10H10ZM8 8H6V6H8V8ZM6 6H4V4H6V6ZM20 6H18V4H20V6ZM4 4H2V2H4V4ZM13 2H18V4H6V2H11V0H13V2ZM22 4H20V2H22V4Z\"/>","book-open":"<path d=\"M2 3h9v2H2zM0 19h11v2H0zM13 3h9v2h-9zm0 16h11v2H13zM11 5h2v18h-2zM0 5h2v14H0zm22 0h2v14h-2zm-7 2h5v2h-5zm0 4h5v2h-5zm0 4h2v2h-2z\"/>","trash":"<path d=\"M18 22H6V20H18V22ZM9 6H15V4H17V6H22V8H20V20H18V8H6V20H4V8H2V6H7V4H9V6ZM15 4H9V2H15V4Z\"/>","magnifying-glass":"<path d=\"M22 22h-2v-2h2v2Zm-2-2h-2v-2h2v2Zm-6-2H6v-2h8v2Zm4 0h-2v-2h2v2ZM6 16H4v-2h2v2Zm10 0h-2v-2h2v2ZM4 14H2V6h2v8Zm14 0h-2V6h2v8ZM6 6H4V4h2v2Zm10 0h-2V4h2v2Zm-2-2H6V2h8v2Z\"/>","terminal-window":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM2 4h2v16H2zm18 0h2v16h-2zM6 16h2v2H6zm2-2h2v2H8zm-2-2h2v2H6z\"/>","stack":"<path d=\"M9 3H7v14h2zM5 7H3v14h2zm12-6H9v2h8zm4 4h-2v12h2zm-2 12H9v2h10zm-4 4H5v2h10zm2-18h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M13 7h6v2h-6zM5 5h2v2H5zm10 14h2v2h-2z\"/>","chart-bar":"<path d=\"M4 20h18v2H4zM2 2h2v18H2zm16 11v3h-2v-3zM8 13v3H6v-3zm8-2v2H8v-2zm0 5v2H8v-2zm4-12v3h-2V4zM8 4v3H6V4zm10-2v2H8V2zm0 5v2H8V7z\"/>","clock-counter-clockwise":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","brain":"<path d=\"M4 6h16v2H4zm0 14h16v2H4zM2 8h2v12H2zm18 0h2v12h-2z\"/>\n <path d=\"M11 4h2v4h-2zm-3 6h2v2H8zm6 0h4v2h-4zm-1-8h4v2h-4zM0 12h2v2H0zm22 0h2v2h-2zm-12 4h4v2h-4zm-2-2h2v2H8zm6 0h2v2h-2z\"/>","eye":"<path d=\"M16 20H8v-2h8v2Zm-8-2H4v-2h4v2Zm12 0h-4v-2h4v2ZM4 16H2v-2h2v2Zm10-6h-2v2h2v-2h2v4h-2v2h-4v-2H8v-4h2V8h4v2Zm8 6h-2v-2h2v2ZM2 14H0v-4h2v4Zm22 0h-2v-4h2v4ZM4 10H2V8h2v2Zm18 0h-2V8h2v2ZM8 8H4V6h4v2Zm12 0h-4V6h4v2Zm-4-2H8V4h8v2Z\"/>","pause":"<path d=\"M10 20H4V4h6v16Zm8-16v16h-6V4h6Zm-4 2v12h2V6h-2ZM6 18h2V6H6v12Z\"/>","stop":"<path d=\"M20 20H4V4H20V20ZM6 18H18V6H6V18ZM14 14H10V10H14V14Z\"/>","broadcast":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM20 4h2v16h-2zM2 4h2v16H2zm5 11h2v2H7zm0-4h4v2H7zm0-4h6v2H7zm4 6h2v4h-2zm4-2h2v6h-2zm-2-2h2v2h-2z\"/>","plugs-connected":"<path d=\"M16 18h-3v4h-2v-4H8v-2h8v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-8-9h4V2h2v5h5v2h-1v5h-2V9H6v5H4V9H3V7h5V2h2v5Z\"/>","plugs":"<path d=\"M16 18h-3v4h-2v-4H8v-2h8v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-8-9h4V2h2v5h5v2h-1v5h-2V9H6v5H4V9H3V7h5V2h2v5Z\"/>","currency-dollar":"<path d=\"M17 18h-4v4h-2v-4H7v-2h10v2Zm2-2h-2v-3h2v3Zm-2-3H7v-2h10v2ZM7 11H5V8h2v3Zm6-5h4v2H7V6h4V2h2v4Z\"/>","rows":"<path d=\"M10 5h12v2H10zm0 4h8v2h-8zm0 4h12v2H10zm0 4h8v2h-8zm-4-6H4V9h2v2ZM4 9H2V7h2v2Zm4 0H6V7h2v2ZM6 7H4V5h2v2Zm-2 6h2v2H4zm0 4h2v2H4zm-2 0v-2h2v2zm4 0v-2h2v2z\"/>","list-bullets":"<path d=\"M10 5h12v2H10zm0 4h8v2h-8zm0 4h12v2H10zm0 4h8v2h-8zm-4-6H4V9h2v2ZM4 9H2V7h2v2Zm4 0H6V7h2v2ZM6 7H4V5h2v2Zm-2 6h2v2H4zm0 4h2v2H4zm-2 0v-2h2v2zm4 0v-2h2v2z\"/>","sliders":"<path d=\"M8 14H7v6H5v-6H2v-2h6v2Zm5 6h-2V10h2v10Zm9-2h-3v2h-2v-2h-1v-2h6v2Zm-3-4h-2V4h2v10ZM7 10H5V4h2v6Zm6-4h2v2H9V6h2V4h2v2Z\"/>","comment-text":"<path d=\"M4 2h16v2H4zm0 14h14v2H4zm2-6h6v2H6zm0-4h12v2H6zM2 4h2v12H2zm18 0h2v18h-2zm-2 14h2v2h-2z\"/>","arrow-bar-left":"<path d=\"M4 4v16H2V4zm18 7v2H6v-2zm-12 2v2H8v-2zm2 2v2h-2v-2zm2 2v2h-2v-2zm-4-8v2H8V9zm2-2v2h-2V7zm2-2v2h-2V5z\"/>","arrow-bar-right":"<path d=\"M20 4v16h2V4zM2 11v2h16v-2zm12 2v2h2v-2zm-2 2v2h2v-2zm-2 2v2h2v-2zm4-8v2h2V9zm-2-2v2h2V7zm-2-2v2h2V5z\"/>","git-pull-request":"<path d=\"M2 10h8V8H2zm0-6h8V2H2zm0 4h2V4H2zm6 0h2V4H8zm6 14h8v-2h-8zm0-6h8v-2h-8zm0 4h2v-4h-2zm6 0h2v-4h-2zM12 7h5V5h-5zM5 22h2V12H5z\"/>","star":"<path d=\"M5 20H8V22H3V16H5V20ZM21 22H16V20H19V16H21V22ZM10 20H8V18H10V20ZM16 20H14V18H16V20ZM14 18H10V16H14V18ZM7 16H5V13H7V16ZM19 16H17V13H19V16ZM5 13H3V11H5V13ZM21 13H19V11H21V13ZM9 9H3V11H1V7H9V9ZM23 11H21V9H15V7H23V11ZM11 7H9V3H11V7ZM15 7H13V3H15V7ZM13 3H11V1H13V3Z\"/>","shield":"<path d=\"M4 2h16v2H4zM2 4h2v10H2zm18 0h2v10h-2zM4 14h2v2H4zm2 2h2v2H6zm4 4h4v2h-4zm10-6h-2v2h2zm-2 2h-2v2h2zm-2 2h-2v2h2zm-6 0H8v2h2z\"/>","bell":"<path d=\"M9 2h6v2H9zM7 4h2v2H7zm8 0h2v2h-2zM5 6h2v7H5zm12 0h2v7h-2zM3 13h2v4H3zm16 0h2v4h-2z\"/>\n <path d=\"M3 15h18v2H3zm5 3h2v2H8zm6 0h2v2h-2zm-4 2h4v2h-4z\"/>","arrow-right":"<path d=\"M4 11v2h16v-2zm12 2v2h2v-2zm-2 2v2h2v-2zm-2 2v2h2v-2zm4-6V9h2v2z\"/>\n <path d=\"M14 15V7h2v8zm-2 2V5h2v12z\"/>"};
|
|
3
3
|
window.icon = (name, size = 16, cls = "") => { const p = ICON_PATHS[name]; if (!p) return ""; return `<svg class="ph ${cls}" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" shape-rendering="crispEdges" aria-hidden="true">${p}</svg>`; };
|
|
4
|
-
window.ICON_NAMES = ["squares-four","timer","coins","pulse","folder-simple","folders","push-pin","push-pin-slash","x","plus","dots-three","dots-three-vertical","keyboard","play","tree-structure","robot","coin","cpu","arrows-clockwise","lightning","clock","wrench","file-text","git-branch","git-commit","arrow-left","warning","check","copy","arrow-square-out","moon","sun","monitor","gear","book-open","trash","magnifying-glass","terminal-window","stack","chart-bar","clock-counter-clockwise","brain","eye","pause","stop","broadcast","plugs-connected","plugs","currency-dollar","rows","list-bullets","sliders","comment-text","arrow-bar-left","arrow-bar-right","git-pull-request","star","arrow-right"];
|
|
4
|
+
window.ICON_NAMES = ["squares-four","timer","coins","pulse","folder-simple","folders","push-pin","push-pin-slash","x","plus","dots-three","dots-three-vertical","keyboard","play","tree-structure","robot","coin","cpu","arrows-clockwise","lightning","clock","wrench","file-text","git-branch","git-commit","arrow-left","warning","check","copy","arrow-square-out","moon","sun","monitor","gear","book-open","trash","magnifying-glass","terminal-window","stack","chart-bar","clock-counter-clockwise","brain","eye","pause","stop","broadcast","plugs-connected","plugs","currency-dollar","rows","list-bullets","sliders","comment-text","arrow-bar-left","arrow-bar-right","git-pull-request","star","shield","bell","arrow-right"];
|
package/web/index.html
CHANGED
|
@@ -141,6 +141,18 @@
|
|
|
141
141
|
.proj:hover small{display:none}
|
|
142
142
|
.proj .act:hover{color:var(--fg);background:var(--line)}
|
|
143
143
|
.proj .act.rm:hover{color:var(--bad)}
|
|
144
|
+
.srch{display:flex;gap:8px;margin-bottom:14px}
|
|
145
|
+
.srch input{flex:1;max-width:640px;background:var(--panel);border:1px solid var(--line);color:var(--fg);border-radius:var(--r-sm);padding:8px 12px;font:var(--fs-md) var(--sans)}
|
|
146
|
+
.srch input:focus{border-color:var(--acc);outline:none}
|
|
147
|
+
.hit{padding:10px 12px;border:1px solid var(--line);border-radius:var(--r-sm);background:var(--panel);margin-bottom:8px}
|
|
148
|
+
.hit .ht{display:flex;gap:8px;align-items:center;margin-bottom:4px}
|
|
149
|
+
.hit .hs{font-size:var(--fs-sm);color:var(--fg-2);white-space:pre-wrap}
|
|
150
|
+
.hit mark{background:var(--warn-soft);color:var(--fg);border-radius:2px;padding:0 1px}
|
|
151
|
+
.dr-rules{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 16px;margin:10px 0}
|
|
152
|
+
.dr-rule{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm)}
|
|
153
|
+
.dr-rule select{background:var(--panel-2);color:var(--fg);border:1px solid var(--line);border-radius:var(--r-sm);padding:2px 4px}
|
|
154
|
+
.dr-flaky{padding:6px 8px;border:1px solid var(--line);border-radius:var(--r-sm);margin:6px 0;background:var(--panel-2)}
|
|
155
|
+
table.plain{width:100%;border-collapse:collapse;font-size:var(--fs-sm)} table.plain td{padding:3px 6px;border-top:1px solid var(--line);vertical-align:top}
|
|
144
156
|
#picker:empty{display:none}
|
|
145
157
|
#picker{position:fixed;inset:0;background:var(--overlay);display:grid;place-items:center;z-index:50}
|
|
146
158
|
.pk{width:min(560px,92vw);max-height:78vh;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow-pop);overflow:hidden}
|
|
@@ -156,6 +168,21 @@
|
|
|
156
168
|
.pk-b textarea{min-height:120px;resize:vertical;font-family:var(--mono);font-size:var(--fs-sm);line-height:1.5}
|
|
157
169
|
.pk-b input:focus,.pk-b textarea:focus,.pk-b select:focus{border-color:var(--acc)}
|
|
158
170
|
.pk-b .row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
|
|
171
|
+
/* What's New (release notes) */
|
|
172
|
+
.wn .pk-b{max-height:64vh;overflow:auto;gap:2px}
|
|
173
|
+
.wn h3{margin:0 0 2px;font:600 var(--fs-lg) var(--sans);color:var(--fg)}
|
|
174
|
+
.wn .date{color:var(--dim);font-size:var(--fs-sm);margin-bottom:12px}
|
|
175
|
+
.wn h4{margin:14px 0 4px;font:600 var(--fs-md) var(--sans);color:var(--acc);text-transform:none;letter-spacing:0}
|
|
176
|
+
.wn ul{margin:0 0 6px;padding-left:18px}
|
|
177
|
+
.wn li{margin:5px 0;line-height:1.55;color:var(--fg-2)}
|
|
178
|
+
.wn p{margin:0 0 10px;color:var(--fg-2);line-height:1.6}
|
|
179
|
+
.wn code{font-family:var(--mono);font-size:var(--fs-sm);background:var(--panel-2);border-radius:var(--r-xs);padding:1px 5px}
|
|
180
|
+
.wn a{color:var(--acc)}
|
|
181
|
+
.wn b{color:var(--fg)}
|
|
182
|
+
.wn .snip{background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 11px;font-family:var(--mono);font-size:var(--fs-sm);line-height:1.5;white-space:pre-wrap;word-break:break-word;margin:2px 0 12px;color:var(--fg-2)}
|
|
183
|
+
.wn .cbtn{font-weight:500;font-size:var(--fs-sm);color:var(--acc);margin-left:8px}
|
|
184
|
+
.rp .snip{max-height:32vh;overflow:auto}
|
|
185
|
+
.rp .pk-f input[type=range]{accent-color:var(--acc)}
|
|
159
186
|
/* pending permission prompts (M3.2) */
|
|
160
187
|
.perm{background:var(--warn-soft);border:1px solid var(--warn);border-radius:var(--r-sm);padding:10px 12px;margin-top:10px}
|
|
161
188
|
.perm-t{font:500 var(--fs-md) var(--sans);color:var(--fg)}
|
|
@@ -445,6 +472,7 @@
|
|
|
445
472
|
<a href="#" data-view="timeline" class="nav"><i data-icon="clock-counter-clockwise"></i>Timeline</a>
|
|
446
473
|
<a href="#" data-view="spend" class="nav"><i data-icon="coins"></i>Spend</a>
|
|
447
474
|
<a href="#" data-view="stats" class="nav"><i data-icon="chart-bar"></i>Stats</a>
|
|
475
|
+
<a href="#" data-view="search" class="nav"><i data-icon="magnifying-glass"></i>Search</a>
|
|
448
476
|
<span class="sp"></span>
|
|
449
477
|
<span id="today"></span>
|
|
450
478
|
<span id="daemon"><span class="dot"></span><span class="lbl">Daemon</span></span>
|
|
@@ -457,6 +485,7 @@
|
|
|
457
485
|
<main id="main"></main>
|
|
458
486
|
<div id="picker"></div>
|
|
459
487
|
<script src="/icons.js"></script>
|
|
488
|
+
<script src="/release-notes.js"></script>
|
|
460
489
|
<script src="/viz.js"></script>
|
|
461
490
|
<script src="/table.js"></script>
|
|
462
491
|
<script src="/app.js"></script>
|