lazyclaw 6.0.1 → 6.1.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/README.ko.md +88 -25
- package/README.md +121 -190
- package/channels/handoff.mjs +18 -5
- package/channels/matrix.mjs +23 -5
- package/channels/slack.mjs +83 -50
- package/channels/slack_env.mjs +45 -0
- package/channels/telegram.mjs +49 -6
- package/cli.mjs +10 -21
- package/commands/channels.mjs +106 -107
- package/commands/chat.mjs +81 -45
- package/commands/daemon.mjs +15 -0
- package/commands/gateway.mjs +194 -0
- package/commands/providers.mjs +17 -38
- package/commands/service.mjs +113 -0
- package/commands/setup.mjs +55 -54
- package/commands/setup_channels.mjs +207 -0
- package/config_features.mjs +106 -0
- package/daemon/lib/inbound_dedup.mjs +108 -0
- package/daemon/lib/learn_queue.mjs +46 -0
- package/daemon/routes/_deps.mjs +6 -0
- package/daemon/routes/conversation.mjs +68 -4
- package/daemon/routes/ops.mjs +9 -29
- package/dotenv_min.mjs +28 -0
- package/first_run.mjs +9 -0
- package/lib/gateway_guard.mjs +100 -0
- package/lib/inbound_client.mjs +108 -0
- package/lib/service_install.mjs +207 -0
- package/package.json +2 -3
- package/providers/probe.mjs +28 -0
- package/tui/editor.mjs +18 -2
- package/tui/repl.mjs +25 -4
- package/tui/slash_commands.mjs +4 -0
- package/tui/slash_dispatcher.mjs +118 -0
- package/tui/splash_props.mjs +52 -0
- package/web/dashboard.css +6 -12
- package/web/dashboard.html +2 -34
- package/web/dashboard.js +3 -3
package/tui/slash_dispatcher.mjs
CHANGED
|
@@ -1670,6 +1670,119 @@ async function _menu(args, ctx) {
|
|
|
1670
1670
|
].join('\n');
|
|
1671
1671
|
}
|
|
1672
1672
|
|
|
1673
|
+
// /channels — view configured channels and toggle them. `/channels` lists;
|
|
1674
|
+
// `/channels <name> on|off` enables/disables. Reads/writes cfg via ctx when
|
|
1675
|
+
// available, else lib/config directly, so it works on both REPL paths.
|
|
1676
|
+
async function _channels(args, ctx = {}) {
|
|
1677
|
+
const cf = await import('../config_features.mjs');
|
|
1678
|
+
const cfgMod = await import('../lib/config.mjs');
|
|
1679
|
+
const read = typeof ctx.readConfig === 'function' ? ctx.readConfig : cfgMod.readConfig;
|
|
1680
|
+
const write = typeof ctx.writeConfig === 'function' ? ctx.writeConfig : cfgMod.writeConfig;
|
|
1681
|
+
const [name, action] = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
1682
|
+
if (name && /^(on|off|enable|disable)$/i.test(action || '')) {
|
|
1683
|
+
const en = /^(on|enable)$/i.test(action);
|
|
1684
|
+
const cfg = read();
|
|
1685
|
+
const key = name.toLowerCase();
|
|
1686
|
+
// Reject unknown names so a typo can't silently create a bogus
|
|
1687
|
+
// cfg.channels.<name> section (which would then leak into the list).
|
|
1688
|
+
// Stay permissive for pre-existing custom sections.
|
|
1689
|
+
const existing = (cfg.channels && typeof cfg.channels === 'object') ? cfg.channels : {};
|
|
1690
|
+
if (!cf.KNOWN_CHANNELS.includes(key) && !(key in existing)) {
|
|
1691
|
+
return `unknown channel: ${key} (known: ${cf.KNOWN_CHANNELS.join(', ')})`;
|
|
1692
|
+
}
|
|
1693
|
+
cf.channelSetEnabled(cfg, key, en); write(cfg);
|
|
1694
|
+
// Legacy fallback path: the readline ctx (_legacyCtx) has no
|
|
1695
|
+
// readConfig/writeConfig, so we read/wrote disk above against a fresh
|
|
1696
|
+
// cfg object. Mirror the toggle onto the in-session ctx.cfg so a
|
|
1697
|
+
// follow-up `/channels` (list) or other in-session read stays
|
|
1698
|
+
// consistent instead of showing the stale pre-toggle value.
|
|
1699
|
+
if (ctx.cfg && ctx.cfg !== cfg && typeof ctx.cfg === 'object') {
|
|
1700
|
+
cf.channelSetEnabled(ctx.cfg, key, en);
|
|
1701
|
+
}
|
|
1702
|
+
return `channel ${key} → ${en ? 'enabled' : 'disabled'}`;
|
|
1703
|
+
}
|
|
1704
|
+
const rows = cf.channelStatusList(read());
|
|
1705
|
+
if (!rows.length) return 'no channels configured. add creds with /config (re-runs setup) or `lazyclaw setup`.';
|
|
1706
|
+
const lines = ['configured channels:'];
|
|
1707
|
+
for (const c of rows) lines.push(` ${c.name} ${c.enabled ? 'enabled' : 'disabled'}${c.boundAgent ? ' · agent: ' + c.boundAgent : ''}`);
|
|
1708
|
+
lines.push('toggle: /channels <name> on|off · add creds: /config');
|
|
1709
|
+
return lines.join('\n');
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// /orchestrator — view/edit multi-agent config. `status` (default), `on`/`off`,
|
|
1713
|
+
// `planner <spec>`, `worker add|remove <spec>`, `maxsubtasks <N>`. ctx-or-
|
|
1714
|
+
// lib/config fallback so it works on both REPL paths.
|
|
1715
|
+
async function _orchestrator(args, ctx = {}) {
|
|
1716
|
+
const cf = await import('../config_features.mjs');
|
|
1717
|
+
const cfgMod = await import('../lib/config.mjs');
|
|
1718
|
+
const read = typeof ctx.readConfig === 'function' ? ctx.readConfig : cfgMod.readConfig;
|
|
1719
|
+
const write = typeof ctx.writeConfig === 'function' ? ctx.writeConfig : cfgMod.writeConfig;
|
|
1720
|
+
const persist = (cfg) => { write(cfg); if (ctx.cfg) ctx.cfg = cfg; };
|
|
1721
|
+
const parts = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
1722
|
+
const fmt = () => {
|
|
1723
|
+
const s = cf.orchestratorGet(read());
|
|
1724
|
+
return `orchestrator: ${s.active ? 'ON' : 'off'} · planner: ${s.planner || '(default)'} · workers: ${s.workers.length ? s.workers.join(', ') : '(none)'} · maxSubtasks: ${s.maxSubtasks}`;
|
|
1725
|
+
};
|
|
1726
|
+
// Bare `/orchestrator` → arrow-key picker (Ink). Pick ON/OFF/Status instead
|
|
1727
|
+
// of typing the subcommand. Legacy path (no openPicker) shows status text.
|
|
1728
|
+
if (parts.length === 0 && typeof ctx.openPicker === 'function') {
|
|
1729
|
+
const s = cf.orchestratorGet(read());
|
|
1730
|
+
const picked = await ctx.openPicker({
|
|
1731
|
+
title: 'Orchestration',
|
|
1732
|
+
subtitle: `now ${s.active ? 'ON' : 'off'} · planner ${s.planner || '(default)'} · ${s.workers.length} worker(s)`,
|
|
1733
|
+
items: [
|
|
1734
|
+
{ id: 'on', label: 'Turn ON', desc: 'route chats through planner + workers' },
|
|
1735
|
+
{ id: 'off', label: 'Turn OFF', desc: 'back to a single provider' },
|
|
1736
|
+
{ id: 'status', label: 'Status', desc: 'show current config' },
|
|
1737
|
+
],
|
|
1738
|
+
});
|
|
1739
|
+
if (!picked || typeof picked !== 'string') return fmt();
|
|
1740
|
+
return _orchestrator(picked, ctx);
|
|
1741
|
+
}
|
|
1742
|
+
const sub = (parts[0] || 'status').toLowerCase();
|
|
1743
|
+
if (sub === 'status') return fmt();
|
|
1744
|
+
const cfg = read();
|
|
1745
|
+
if (sub === 'on' || sub === 'enable') {
|
|
1746
|
+
if (!cf.orchestratorGet(cfg).planner) {
|
|
1747
|
+
const base = cfg.provider && cfg.provider !== 'orchestrator' ? cfg.provider : 'claude-cli';
|
|
1748
|
+
cf.orchestratorSet(cfg, { planner: base });
|
|
1749
|
+
}
|
|
1750
|
+
cf.orchestratorEnable(cfg, true); persist(cfg);
|
|
1751
|
+
const after = cf.orchestratorGet(read());
|
|
1752
|
+
return after.workers.length ? 'orchestration ON.\n' + fmt() : 'orchestration ON — but no workers yet. Add one: /orchestrator worker add <provider[:model]>';
|
|
1753
|
+
}
|
|
1754
|
+
if (sub === 'off' || sub === 'disable') { cf.orchestratorEnable(cfg, false); persist(cfg); return 'orchestration off. provider → ' + read().provider; }
|
|
1755
|
+
if (sub === 'planner') { if (!parts[1]) return 'usage: /orchestrator planner <provider[:model]>'; cf.orchestratorSet(cfg, { planner: parts[1] }); persist(cfg); return 'planner → ' + parts[1]; }
|
|
1756
|
+
if (sub === 'maxsubtasks') { const n = parseInt(parts[1], 10); if (!Number.isFinite(n)) return 'usage: /orchestrator maxsubtasks <N>'; cf.orchestratorSet(cfg, { maxSubtasks: Math.max(1, Math.min(10, n)) }); persist(cfg); return fmt(); }
|
|
1757
|
+
if (sub === 'worker') {
|
|
1758
|
+
const action = (parts[1] || '').toLowerCase(); const spec = parts[2];
|
|
1759
|
+
const workers = [...cf.orchestratorGet(cfg).workers];
|
|
1760
|
+
if (action === 'add' && spec) { if (!workers.includes(spec)) workers.push(spec); cf.orchestratorSet(cfg, { workers }); persist(cfg); return 'workers: ' + workers.join(', '); }
|
|
1761
|
+
if ((action === 'remove' || action === 'rm') && spec) { const next = workers.filter((w) => w !== spec); cf.orchestratorSet(cfg, { workers: next }); persist(cfg); return 'workers: ' + (next.join(', ') || '(none)'); }
|
|
1762
|
+
return 'usage: /orchestrator worker add|remove <provider[:model]>';
|
|
1763
|
+
}
|
|
1764
|
+
return 'usage: /orchestrator [status|on|off|planner <spec>|worker add|remove <spec>|maxsubtasks <N>]';
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// /context — view/set the chat history window (turns + token budget). This is
|
|
1768
|
+
// the sliding history budget sent each turn, NOT the model's hard context
|
|
1769
|
+
// limit. ctx-or-lib/config fallback so it works on both REPL paths.
|
|
1770
|
+
async function _context(args, ctx = {}) {
|
|
1771
|
+
const cf = await import('../config_features.mjs');
|
|
1772
|
+
const cfgMod = await import('../lib/config.mjs');
|
|
1773
|
+
const read = typeof ctx.readConfig === 'function' ? ctx.readConfig : cfgMod.readConfig;
|
|
1774
|
+
const write = typeof ctx.writeConfig === 'function' ? ctx.writeConfig : cfgMod.writeConfig;
|
|
1775
|
+
const persist = (cfg) => { write(cfg); if (ctx.cfg) ctx.cfg = cfg; };
|
|
1776
|
+
const parts = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
1777
|
+
const sub = (parts[0] || 'status').toLowerCase();
|
|
1778
|
+
const fmt = () => { const w = cf.chatWindowGet(read()); return `context window: ${w.turns} turns · ${w.tokens} tokens (history budget — not the model's hard limit)`; };
|
|
1779
|
+
if (sub === 'status') return fmt();
|
|
1780
|
+
const n = parseInt(parts[1], 10);
|
|
1781
|
+
if (sub === 'turns') { if (!Number.isFinite(n) || n < 1) return 'usage: /context turns <N>'; const cfg = read(); cf.chatWindowSet(cfg, { turns: n }); persist(cfg); return fmt(); }
|
|
1782
|
+
if (sub === 'tokens') { if (!Number.isFinite(n) || n < 256) return 'usage: /context tokens <N> (min 256)'; const cfg = read(); cf.chatWindowSet(cfg, { tokens: n }); persist(cfg); return fmt(); }
|
|
1783
|
+
return 'usage: /context [status | turns <N> | tokens <N>]';
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1673
1786
|
// ─── dispatch table ──────────────────────────────────────────────────────
|
|
1674
1787
|
|
|
1675
1788
|
export const SLASH_HANDLERS = new Map([
|
|
@@ -1698,6 +1811,11 @@ export const SLASH_HANDLERS = new Map([
|
|
|
1698
1811
|
['/trainer', _trainer],
|
|
1699
1812
|
['/dashboard', _dashboard],
|
|
1700
1813
|
['/menu', _menu],
|
|
1814
|
+
['/channels', _channels],
|
|
1815
|
+
['/orchestrator', _orchestrator],
|
|
1816
|
+
['/context', _context],
|
|
1817
|
+
// /config — unmount and let chat.mjs run the setup wizard (ctx.requestSetup).
|
|
1818
|
+
['/config', async (_a, ctx) => { ctx.requestSetup = true; return 'EXIT'; }],
|
|
1701
1819
|
['/exit', async () => 'EXIT'],
|
|
1702
1820
|
['/quit', async () => 'EXIT'],
|
|
1703
1821
|
]);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// tui/splash_props.mjs — gather the dynamic props the splash panel needs
|
|
2
|
+
// (tool groups + skill groups), shared by the chat REPL and the setup wizard
|
|
3
|
+
// so both surfaces render the same lazyclaw splash instead of drifting apart
|
|
4
|
+
// (the setup wizard used to show a small figlet banner instead).
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { configPath } from '../lib/config.mjs';
|
|
7
|
+
|
|
8
|
+
// Re-export the renderer so setup/onboarding callers need one import.
|
|
9
|
+
export { renderSplashToString } from './splash.mjs';
|
|
10
|
+
|
|
11
|
+
// Verbatim from the chat REPL's former inline block: collapse the v5 tool
|
|
12
|
+
// registry to one row per category, and group installed skills by their
|
|
13
|
+
// filename hyphen-prefix. Failures degrade to empty lists, never throw.
|
|
14
|
+
export async function gatherToolAndSkillGroups(cfgDir) {
|
|
15
|
+
let tools = [];
|
|
16
|
+
try {
|
|
17
|
+
const registry = await import('../mas/tools/registry.mjs');
|
|
18
|
+
const byCat = registry.byCategory();
|
|
19
|
+
tools = Object.entries(byCat).map(([category, items]) => ({
|
|
20
|
+
category,
|
|
21
|
+
sensitive: items.some((t) => t.sensitive),
|
|
22
|
+
verbs: items.map((t) => t.name.replace(/^[a-z]+_/, '')).slice(0, 6),
|
|
23
|
+
})).sort((a, b) => a.category.localeCompare(b.category));
|
|
24
|
+
} catch { /* registry unavailable → empty list */ }
|
|
25
|
+
|
|
26
|
+
let skills = [];
|
|
27
|
+
try {
|
|
28
|
+
const { listSkills } = await import('../skills.mjs');
|
|
29
|
+
const flat = listSkills(cfgDir);
|
|
30
|
+
const byGroup = new Map();
|
|
31
|
+
for (const s of flat) {
|
|
32
|
+
const i = s.name.indexOf('-');
|
|
33
|
+
const group = i > 0 ? s.name.slice(0, i) : 'general';
|
|
34
|
+
const sub = i > 0 ? s.name.slice(i + 1) : s.name;
|
|
35
|
+
if (!byGroup.has(group)) byGroup.set(group, []);
|
|
36
|
+
byGroup.get(group).push(sub);
|
|
37
|
+
}
|
|
38
|
+
skills = [...byGroup.entries()]
|
|
39
|
+
.map(([group, names]) => ({ group, names: names.slice(0, 6) }))
|
|
40
|
+
.sort((a, b) => a.group.localeCompare(b.group));
|
|
41
|
+
} catch { /* skills dir unavailable → empty list */ }
|
|
42
|
+
|
|
43
|
+
return { tools, skills };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build the full splash props for the setup wizard. Self-contained (resolves
|
|
47
|
+
// the config dir itself) so call sites stay one line.
|
|
48
|
+
export async function splashPropsForSetup({ version = '', provider = '', model = '' } = {}) {
|
|
49
|
+
const cfgDir = path.dirname(configPath());
|
|
50
|
+
const { tools, skills } = await gatherToolAndSkillGroups(cfgDir);
|
|
51
|
+
return { provider, model, trainer: {}, sessionId: '', cwd: process.cwd(), version, tools, skills };
|
|
52
|
+
}
|
package/web/dashboard.css
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
--border: #2a2a36;
|
|
5
5
|
--text: #e8e8ea;
|
|
6
6
|
--dim: #a8a8b8;
|
|
7
|
-
--accent: #
|
|
7
|
+
--accent: #d9b35a;
|
|
8
|
+
--accent-ink: #1a1610;
|
|
8
9
|
--ok: #4ade80;
|
|
9
10
|
--warn: #f59e0b;
|
|
10
11
|
--err: #ef4444;
|
|
@@ -27,14 +28,7 @@
|
|
|
27
28
|
gap: 14px;
|
|
28
29
|
}
|
|
29
30
|
.logo { font-weight: 700; font-size: 16px; color: var(--accent); display: flex; align-items: center; gap: 10px; }
|
|
30
|
-
.logo .mascot { width: 44px; height: 44px; flex: none; image-rendering: pixelated; image-rendering: crisp-edges; }
|
|
31
31
|
.ver { color: var(--dim); font-size: 11px; }
|
|
32
|
-
/* lazyclaw 16x16 pixel mascot — Claude Design handoff (mascot sheet
|
|
33
|
-
v0.1, "claude original" palette). Claude's asterisk star (#d97757)
|
|
34
|
-
worn under a crustacean helmet (#c33d2a) with two antenna-claws.
|
|
35
|
-
Idle pose (sleepy slits). Hover gently brightens the helmet. */
|
|
36
|
-
.mascot { transition: filter 0.2s ease; }
|
|
37
|
-
.logo:hover .mascot { filter: drop-shadow(0 0 6px rgba(217, 119, 87, 0.45)); }
|
|
38
32
|
nav.tabs {
|
|
39
33
|
display: flex;
|
|
40
34
|
flex-wrap: wrap;
|
|
@@ -81,7 +75,7 @@
|
|
|
81
75
|
.dim { color: var(--dim); font-size: 12px; }
|
|
82
76
|
button.btn {
|
|
83
77
|
background: var(--accent);
|
|
84
|
-
color:
|
|
78
|
+
color: var(--accent-ink);
|
|
85
79
|
border: 0;
|
|
86
80
|
border-radius: 6px;
|
|
87
81
|
padding: 8px 14px;
|
|
@@ -119,7 +113,7 @@
|
|
|
119
113
|
gap: 14px;
|
|
120
114
|
}
|
|
121
115
|
.msg { padding: 8px 12px; border-radius: 6px; max-width: 90%; white-space: pre-wrap; word-wrap: break-word; }
|
|
122
|
-
.msg.user { align-self: flex-end; background: rgba(217,
|
|
116
|
+
.msg.user { align-self: flex-end; background: rgba(217, 179, 90, 0.15); border: 1px solid rgba(217, 179, 90, 0.3); }
|
|
123
117
|
.msg.assistant { align-self: flex-start; background: rgba(74, 222, 128, 0.06); border: 1px solid rgba(74, 222, 128, 0.18); }
|
|
124
118
|
.msg.error { align-self: stretch; background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.3); color: #ffd3d3; }
|
|
125
119
|
.input-row {
|
|
@@ -230,9 +224,9 @@
|
|
|
230
224
|
.modal-body { padding: 16px 18px; overflow-y: auto; flex: 1; }
|
|
231
225
|
.modal-foot { padding: 12px 18px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; }
|
|
232
226
|
.clickable { cursor: pointer; }
|
|
233
|
-
.clickable:hover { background: rgba(217,
|
|
227
|
+
.clickable:hover { background: rgba(217, 179, 90, 0.05); }
|
|
234
228
|
.turn { padding: 8px 12px; border-radius: 6px; margin-bottom: 8px; white-space: pre-wrap; word-wrap: break-word; font-size: 13px; }
|
|
235
|
-
.turn.user { background: rgba(217,
|
|
229
|
+
.turn.user { background: rgba(217, 179, 90, 0.10); border: 1px solid rgba(217, 179, 90, 0.25); }
|
|
236
230
|
.turn.assistant { background: rgba(74, 222, 128, 0.06); border: 1px solid rgba(74, 222, 128, 0.18); }
|
|
237
231
|
.turn.system { background: rgba(245, 158, 11, 0.06); border: 1px solid rgba(245, 158, 11, 0.20); }
|
|
238
232
|
.turn .role-tag { display: block; color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
package/web/dashboard.html
CHANGED
|
@@ -14,43 +14,11 @@
|
|
|
14
14
|
<meta charset="utf-8">
|
|
15
15
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
16
16
|
<title>LazyClaw</title>
|
|
17
|
-
<link rel="stylesheet" href="
|
|
17
|
+
<link rel="stylesheet" href="dashboard.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<header>
|
|
21
21
|
<div class="logo">
|
|
22
|
-
<svg class="mascot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges" aria-label="lazyclaw mascot" role="img">
|
|
23
|
-
<!-- antennae / claws -->
|
|
24
|
-
<rect x="3" y="1" width="1" height="1" fill="#a02b1c"/>
|
|
25
|
-
<rect x="12" y="1" width="1" height="1" fill="#a02b1c"/>
|
|
26
|
-
<rect x="3" y="2" width="1" height="1" fill="#a02b1c"/>
|
|
27
|
-
<rect x="12" y="2" width="1" height="1" fill="#a02b1c"/>
|
|
28
|
-
<!-- helmet -->
|
|
29
|
-
<rect x="3" y="3" width="10" height="1" fill="#c33d2a"/>
|
|
30
|
-
<rect x="2" y="4" width="12" height="1" fill="#c33d2a"/>
|
|
31
|
-
<rect x="1" y="5" width="2" height="1" fill="#c33d2a"/>
|
|
32
|
-
<rect x="3" y="5" width="1" height="1" fill="#ff8a6a"/>
|
|
33
|
-
<rect x="4" y="5" width="8" height="1" fill="#c33d2a"/>
|
|
34
|
-
<rect x="12" y="5" width="1" height="1" fill="#ff8a6a"/>
|
|
35
|
-
<rect x="13" y="5" width="2" height="1" fill="#c33d2a"/>
|
|
36
|
-
<rect x="1" y="6" width="14" height="1" fill="#c33d2a"/>
|
|
37
|
-
<rect x="2" y="7" width="12" height="1" fill="#c33d2a"/>
|
|
38
|
-
<!-- star body (Claude asterisk) — top edge below helmet brim -->
|
|
39
|
-
<rect x="3" y="8" width="10" height="1" fill="#d97757"/>
|
|
40
|
-
<!-- face row (idle: sleepy slit eyes) -->
|
|
41
|
-
<rect x="2" y="9" width="3" height="1" fill="#d97757"/>
|
|
42
|
-
<rect x="5" y="9" width="2" height="1" fill="#1a1410"/>
|
|
43
|
-
<rect x="7" y="9" width="2" height="1" fill="#d97757"/>
|
|
44
|
-
<rect x="9" y="9" width="2" height="1" fill="#1a1410"/>
|
|
45
|
-
<rect x="11" y="9" width="3" height="1" fill="#d97757"/>
|
|
46
|
-
<!-- star arms + tip -->
|
|
47
|
-
<rect x="1" y="10" width="14" height="1" fill="#d97757"/>
|
|
48
|
-
<rect x="0" y="11" width="16" height="1" fill="#d97757"/>
|
|
49
|
-
<rect x="1" y="12" width="14" height="1" fill="#d97757"/>
|
|
50
|
-
<rect x="3" y="13" width="10" height="1" fill="#d97757"/>
|
|
51
|
-
<rect x="5" y="14" width="6" height="1" fill="#d97757"/>
|
|
52
|
-
<rect x="7" y="15" width="2" height="1" fill="#d97757"/>
|
|
53
|
-
</svg>
|
|
54
22
|
<span>lazyclaw</span>
|
|
55
23
|
</div>
|
|
56
24
|
<div class="ver" id="version">…</div>
|
|
@@ -276,6 +244,6 @@
|
|
|
276
244
|
</div>
|
|
277
245
|
</div>
|
|
278
246
|
|
|
279
|
-
<script src="
|
|
247
|
+
<script src="dashboard.js"></script>
|
|
280
248
|
</body>
|
|
281
249
|
</html>
|
package/web/dashboard.js
CHANGED
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
? `<span class="pill ok" title="trained by ${escHtml(s.trainedBy || 'trainer')}">trained: ${escHtml(s.trainedBy || 'on')}</span>`
|
|
154
154
|
: '';
|
|
155
155
|
const tagAgent = s.agentName
|
|
156
|
-
? `<span class="pill" style="background:rgba(217,
|
|
156
|
+
? `<span class="pill" style="background:rgba(217,179,90,0.18);color:var(--accent);">@${escHtml(s.agentName)}</span>`
|
|
157
157
|
: '';
|
|
158
158
|
const trajBtn = s.trajectoryId
|
|
159
159
|
? `<button class="btn btn-secondary btn-sm" data-action="trajectory" data-traj="${escHtml(s.trajectoryId)}">Trajectory</button>`
|
|
@@ -391,7 +391,7 @@
|
|
|
391
391
|
const tag = p.requiresApiKey
|
|
392
392
|
? '<span class="pill warn">api key</span>'
|
|
393
393
|
: '<span class="pill ok">no key</span>';
|
|
394
|
-
const customTag = p.custom ? ' <span class="pill" style="background:rgba(217,
|
|
394
|
+
const customTag = p.custom ? ' <span class="pill" style="background:rgba(217,179,90,0.18);color:var(--accent);">custom</span>' : '';
|
|
395
395
|
const builtinCompat = p.builtinOpenAICompat ? ' <span class="pill" style="background:rgba(74,222,128,0.12);color:var(--ok);">openai-compat</span>' : '';
|
|
396
396
|
const models = (p.suggestedModels || []).slice(0, 6).join(' · ') || '<span class="dim">(default)</span>';
|
|
397
397
|
const removeBtn = p.custom
|
|
@@ -1221,7 +1221,7 @@
|
|
|
1221
1221
|
const label = md.session_id || md.skill_name || md.trajectory_id || md.topic || '—';
|
|
1222
1222
|
return `<div class="card">
|
|
1223
1223
|
<div class="row" style="border:0;padding:0;">
|
|
1224
|
-
<span class="pill" style="background:rgba(217,
|
|
1224
|
+
<span class="pill" style="background:rgba(217,179,90,0.18);color:var(--accent);">${escHtml(h.scope)}</span>
|
|
1225
1225
|
<div class="name" style="margin-left:8px;">${escHtml(String(label))}</div>
|
|
1226
1226
|
<div class="dim row-actions">bm25 ${Number(h.bm25 || 0).toFixed(2)}</div>
|
|
1227
1227
|
</div>
|