lazyclaw 3.99.5 → 3.99.8
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.md +1 -1
- package/cli.mjs +647 -180
- package/daemon.mjs +28 -0
- package/package.json +1 -1
- package/web/dashboard.html +459 -0
package/daemon.mjs
CHANGED
|
@@ -378,6 +378,34 @@ export function makeHandler(ctx) {
|
|
|
378
378
|
const workflowMatch = url.pathname.match(/^\/workflows\/([^/]+)$/);
|
|
379
379
|
const configKeyMatch = url.pathname.match(/^\/config\/([^/]+)$/);
|
|
380
380
|
switch (true) {
|
|
381
|
+
case route === 'GET /' || route === 'GET /dashboard': {
|
|
382
|
+
// Serve the lazyclaw-only web dashboard (a single static
|
|
383
|
+
// HTML in src/lazyclaw/web/). Co-resident with the JSON
|
|
384
|
+
// API so a single port handles both — no CORS song and
|
|
385
|
+
// dance, no separate static server. Falls back to a
|
|
386
|
+
// helpful text response when the file is missing (someone
|
|
387
|
+
// ran the daemon out of a partial install).
|
|
388
|
+
try {
|
|
389
|
+
const fs = await import('node:fs');
|
|
390
|
+
const path = await import('node:path');
|
|
391
|
+
const url = await import('node:url');
|
|
392
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
393
|
+
const htmlPath = path.join(here, 'web', 'dashboard.html');
|
|
394
|
+
const body = fs.readFileSync(htmlPath, 'utf8');
|
|
395
|
+
res.writeHead(200, {
|
|
396
|
+
'content-type': 'text/html; charset=utf-8',
|
|
397
|
+
'cache-control': 'no-cache',
|
|
398
|
+
});
|
|
399
|
+
return res.end(body);
|
|
400
|
+
} catch (e) {
|
|
401
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
402
|
+
return res.end(
|
|
403
|
+
`lazyclaw daemon is up but the dashboard HTML wasn't found.\n` +
|
|
404
|
+
`Try \`lazyclaw version\` to confirm install integrity, or hit any /api endpoint directly.\n\n` +
|
|
405
|
+
`error: ${e?.message || e}\n`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
381
409
|
case route === 'GET /version':
|
|
382
410
|
return writeJson(res, 200, { version: ctx.version(), nodeVersion: process.version, platform: `${process.platform}-${process.arch}` });
|
|
383
411
|
case route === 'GET /health':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "3.99.
|
|
3
|
+
"version": "3.99.8",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama and orchestrating multi-step LLM workflows. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
lazyclaw dashboard — a single-file SPA served by `lazyclaw daemon`.
|
|
4
|
+
Deliberately framework-free: vanilla DOM, vanilla fetch, no build
|
|
5
|
+
step. The full lazyclaude dashboard is gigantic; this is the
|
|
6
|
+
lazyclaw-only slice (chat / sessions / skills / providers /
|
|
7
|
+
status), aimed at users who installed via `npm i -g lazyclaw` and
|
|
8
|
+
don't want to clone the whole monorepo just to see a UI.
|
|
9
|
+
-->
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14
|
+
<title>LazyClaw</title>
|
|
15
|
+
<style>
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #0a0a0a;
|
|
18
|
+
--card: #14141c;
|
|
19
|
+
--border: #2a2a36;
|
|
20
|
+
--text: #e8e8ea;
|
|
21
|
+
--dim: #a8a8b8;
|
|
22
|
+
--accent: #d97757;
|
|
23
|
+
--ok: #4ade80;
|
|
24
|
+
--warn: #f59e0b;
|
|
25
|
+
--err: #ef4444;
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
body {
|
|
29
|
+
margin: 0;
|
|
30
|
+
font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
}
|
|
37
|
+
header {
|
|
38
|
+
padding: 14px 22px;
|
|
39
|
+
border-bottom: 1px solid var(--border);
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 14px;
|
|
43
|
+
}
|
|
44
|
+
.logo { font-weight: 700; font-size: 16px; color: var(--accent); }
|
|
45
|
+
.ver { color: var(--dim); font-size: 11px; }
|
|
46
|
+
nav.tabs {
|
|
47
|
+
display: flex;
|
|
48
|
+
gap: 2px;
|
|
49
|
+
padding: 0 14px;
|
|
50
|
+
border-bottom: 1px solid var(--border);
|
|
51
|
+
overflow-x: auto;
|
|
52
|
+
}
|
|
53
|
+
nav.tabs button {
|
|
54
|
+
background: none;
|
|
55
|
+
border: 0;
|
|
56
|
+
color: var(--dim);
|
|
57
|
+
padding: 12px 16px;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
border-bottom: 2px solid transparent;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
nav.tabs button:hover { color: var(--text); }
|
|
64
|
+
nav.tabs button.active {
|
|
65
|
+
color: var(--accent);
|
|
66
|
+
border-bottom-color: var(--accent);
|
|
67
|
+
}
|
|
68
|
+
main {
|
|
69
|
+
flex: 1;
|
|
70
|
+
padding: 22px;
|
|
71
|
+
overflow-y: auto;
|
|
72
|
+
}
|
|
73
|
+
section { display: none; }
|
|
74
|
+
section.active { display: block; }
|
|
75
|
+
h2 { margin: 0 0 14px; font-size: 18px; }
|
|
76
|
+
.card {
|
|
77
|
+
background: var(--card);
|
|
78
|
+
border: 1px solid var(--border);
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
padding: 14px 16px;
|
|
81
|
+
margin-bottom: 12px;
|
|
82
|
+
}
|
|
83
|
+
.row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
|
84
|
+
.row + .row { border-top: 1px solid var(--border); }
|
|
85
|
+
.name { font-weight: 600; }
|
|
86
|
+
.dim { color: var(--dim); font-size: 12px; }
|
|
87
|
+
button.btn {
|
|
88
|
+
background: var(--accent);
|
|
89
|
+
color: #fff;
|
|
90
|
+
border: 0;
|
|
91
|
+
border-radius: 6px;
|
|
92
|
+
padding: 8px 14px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
}
|
|
97
|
+
button.btn:hover { filter: brightness(1.1); }
|
|
98
|
+
button.btn-secondary {
|
|
99
|
+
background: transparent;
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
color: var(--text);
|
|
102
|
+
}
|
|
103
|
+
button.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
|
|
104
|
+
.empty { color: var(--dim); padding: 24px; text-align: center; font-style: italic; }
|
|
105
|
+
pre {
|
|
106
|
+
background: #06060c;
|
|
107
|
+
padding: 12px 14px;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
overflow-x: auto;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: #cfcfd6;
|
|
112
|
+
border: 1px solid var(--border);
|
|
113
|
+
}
|
|
114
|
+
/* Chat */
|
|
115
|
+
#chat-stream {
|
|
116
|
+
background: var(--card);
|
|
117
|
+
border: 1px solid var(--border);
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
padding: 12px;
|
|
120
|
+
height: 50vh;
|
|
121
|
+
overflow-y: auto;
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: 14px;
|
|
125
|
+
}
|
|
126
|
+
.msg { padding: 8px 12px; border-radius: 6px; max-width: 90%; white-space: pre-wrap; word-wrap: break-word; }
|
|
127
|
+
.msg.user { align-self: flex-end; background: rgba(217, 119, 87, 0.15); border: 1px solid rgba(217, 119, 87, 0.3); }
|
|
128
|
+
.msg.assistant { align-self: flex-start; background: rgba(74, 222, 128, 0.06); border: 1px solid rgba(74, 222, 128, 0.18); }
|
|
129
|
+
.msg.error { align-self: stretch; background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.3); color: #ffd3d3; }
|
|
130
|
+
.input-row {
|
|
131
|
+
display: flex;
|
|
132
|
+
gap: 8px;
|
|
133
|
+
margin-top: 12px;
|
|
134
|
+
}
|
|
135
|
+
.input-row textarea {
|
|
136
|
+
flex: 1;
|
|
137
|
+
background: var(--card);
|
|
138
|
+
border: 1px solid var(--border);
|
|
139
|
+
border-radius: 6px;
|
|
140
|
+
padding: 10px 12px;
|
|
141
|
+
color: var(--text);
|
|
142
|
+
resize: vertical;
|
|
143
|
+
min-height: 60px;
|
|
144
|
+
font: inherit;
|
|
145
|
+
}
|
|
146
|
+
.pill {
|
|
147
|
+
display: inline-block;
|
|
148
|
+
padding: 2px 8px;
|
|
149
|
+
border-radius: 999px;
|
|
150
|
+
font-size: 10px;
|
|
151
|
+
background: var(--border);
|
|
152
|
+
color: var(--dim);
|
|
153
|
+
margin-left: 6px;
|
|
154
|
+
}
|
|
155
|
+
.pill.ok { background: rgba(74, 222, 128, 0.15); color: var(--ok); }
|
|
156
|
+
.pill.warn { background: rgba(245, 158, 11, 0.15); color: var(--warn); }
|
|
157
|
+
.pill.err { background: rgba(239, 68, 68, 0.15); color: var(--err); }
|
|
158
|
+
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
159
|
+
.toolbar select {
|
|
160
|
+
background: var(--card);
|
|
161
|
+
border: 1px solid var(--border);
|
|
162
|
+
color: var(--text);
|
|
163
|
+
padding: 6px 10px;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
font: inherit;
|
|
166
|
+
}
|
|
167
|
+
footer {
|
|
168
|
+
padding: 10px 22px;
|
|
169
|
+
border-top: 1px solid var(--border);
|
|
170
|
+
color: var(--dim);
|
|
171
|
+
font-size: 11px;
|
|
172
|
+
display: flex;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
}
|
|
175
|
+
</style>
|
|
176
|
+
</head>
|
|
177
|
+
<body>
|
|
178
|
+
<header>
|
|
179
|
+
<div class="logo">🦞 LazyClaw</div>
|
|
180
|
+
<div class="ver" id="version">…</div>
|
|
181
|
+
</header>
|
|
182
|
+
|
|
183
|
+
<nav class="tabs">
|
|
184
|
+
<button data-tab="chat" class="active">Chat</button>
|
|
185
|
+
<button data-tab="sessions">Sessions</button>
|
|
186
|
+
<button data-tab="skills">Skills</button>
|
|
187
|
+
<button data-tab="providers">Providers</button>
|
|
188
|
+
<button data-tab="status">Status</button>
|
|
189
|
+
</nav>
|
|
190
|
+
|
|
191
|
+
<main>
|
|
192
|
+
<section id="tab-chat" class="active">
|
|
193
|
+
<h2>Chat</h2>
|
|
194
|
+
<div class="toolbar">
|
|
195
|
+
<select id="chat-assignee"><option value="">(loading…)</option></select>
|
|
196
|
+
<button class="btn btn-secondary" onclick="resetChat()">Clear</button>
|
|
197
|
+
<span class="dim" id="chat-meta"></span>
|
|
198
|
+
</div>
|
|
199
|
+
<div id="chat-stream"><div class="empty">Type below to start.</div></div>
|
|
200
|
+
<div class="input-row">
|
|
201
|
+
<textarea id="chat-input" placeholder="Send a message — Enter to send, Shift+Enter for newline."></textarea>
|
|
202
|
+
<button class="btn" onclick="sendChat()">Send</button>
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|
|
205
|
+
|
|
206
|
+
<section id="tab-sessions">
|
|
207
|
+
<h2>Sessions</h2>
|
|
208
|
+
<div id="sessions-list"><div class="empty">Loading…</div></div>
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<section id="tab-skills">
|
|
212
|
+
<h2>Skills</h2>
|
|
213
|
+
<div id="skills-list"><div class="empty">Loading…</div></div>
|
|
214
|
+
</section>
|
|
215
|
+
|
|
216
|
+
<section id="tab-providers">
|
|
217
|
+
<h2>Providers</h2>
|
|
218
|
+
<div id="providers-list"><div class="empty">Loading…</div></div>
|
|
219
|
+
</section>
|
|
220
|
+
|
|
221
|
+
<section id="tab-status">
|
|
222
|
+
<h2>Status</h2>
|
|
223
|
+
<div id="status-card"><div class="empty">Loading…</div></div>
|
|
224
|
+
</section>
|
|
225
|
+
</main>
|
|
226
|
+
|
|
227
|
+
<footer>
|
|
228
|
+
<span>lazyclaw dashboard</span>
|
|
229
|
+
<span id="footer-url"></span>
|
|
230
|
+
</footer>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
// Tab switching ────────────────────────────────────────────────
|
|
234
|
+
const tabs = document.querySelectorAll('nav.tabs button');
|
|
235
|
+
const sections = document.querySelectorAll('main section');
|
|
236
|
+
tabs.forEach((b) => b.addEventListener('click', () => {
|
|
237
|
+
tabs.forEach((x) => x.classList.toggle('active', x === b));
|
|
238
|
+
sections.forEach((s) => s.classList.toggle('active', s.id === 'tab-' + b.dataset.tab));
|
|
239
|
+
const loader = LOADERS[b.dataset.tab];
|
|
240
|
+
if (loader) loader();
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
document.getElementById('footer-url').textContent = location.href;
|
|
244
|
+
|
|
245
|
+
// Tiny fetch helper that surfaces errors as toasts on the page.
|
|
246
|
+
async function api(path, opts = {}) {
|
|
247
|
+
const r = await fetch(path, opts);
|
|
248
|
+
if (!r.ok && r.status !== 200) {
|
|
249
|
+
let body = '';
|
|
250
|
+
try { body = JSON.stringify(await r.json()); } catch {}
|
|
251
|
+
throw new Error(`${r.status} ${r.statusText}${body ? ' — ' + body : ''}`);
|
|
252
|
+
}
|
|
253
|
+
return r.json();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Status / version (always shown in header) ────────────────
|
|
257
|
+
api('/version').then((v) => {
|
|
258
|
+
document.getElementById('version').textContent = `v${v.version}`;
|
|
259
|
+
}).catch(() => {});
|
|
260
|
+
|
|
261
|
+
// ── Loaders per tab ──────────────────────────────────────────
|
|
262
|
+
const LOADERS = {};
|
|
263
|
+
|
|
264
|
+
LOADERS.chat = async function loadChat() {
|
|
265
|
+
try {
|
|
266
|
+
const r = await api('/providers');
|
|
267
|
+
const sel = document.getElementById('chat-assignee');
|
|
268
|
+
sel.innerHTML = '';
|
|
269
|
+
for (const p of r.providers || []) {
|
|
270
|
+
const ms = (p.suggestedModels || []);
|
|
271
|
+
if (!ms.length) {
|
|
272
|
+
const opt = document.createElement('option');
|
|
273
|
+
opt.value = p.name; opt.textContent = p.name;
|
|
274
|
+
sel.appendChild(opt);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
for (const m of ms.slice(0, 6)) {
|
|
278
|
+
const opt = document.createElement('option');
|
|
279
|
+
opt.value = `${p.name}:${m}`;
|
|
280
|
+
opt.textContent = `${p.name} · ${m}`;
|
|
281
|
+
sel.appendChild(opt);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (e) {
|
|
285
|
+
document.getElementById('chat-meta').textContent = '⚠ ' + e.message;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
LOADERS.sessions = async function loadSessions() {
|
|
290
|
+
const root = document.getElementById('sessions-list');
|
|
291
|
+
root.innerHTML = '<div class="empty">Loading…</div>';
|
|
292
|
+
try {
|
|
293
|
+
const r = await api('/sessions');
|
|
294
|
+
const arr = r.sessions || r;
|
|
295
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
296
|
+
root.innerHTML = '<div class="empty">No persisted sessions yet. Start one with <code>lazyclaw chat --session <id></code>.</div>';
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
root.innerHTML = '';
|
|
300
|
+
arr.forEach((s) => {
|
|
301
|
+
const div = document.createElement('div');
|
|
302
|
+
div.className = 'card row';
|
|
303
|
+
const id = s.id || s.sessionId || s.name || JSON.stringify(s);
|
|
304
|
+
const turns = s.turns ?? s.turnCount ?? '';
|
|
305
|
+
const updated = s.updatedAt || s.mtime || '';
|
|
306
|
+
div.innerHTML = `<div class="name">${id}</div>
|
|
307
|
+
<div class="dim">${turns ? turns + ' turns' : ''}</div>
|
|
308
|
+
<div class="dim" style="margin-left:auto;">${updated}</div>`;
|
|
309
|
+
root.appendChild(div);
|
|
310
|
+
});
|
|
311
|
+
} catch (e) {
|
|
312
|
+
root.innerHTML = `<div class="empty">⚠ ${e.message}</div>`;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
LOADERS.skills = async function loadSkills() {
|
|
317
|
+
const root = document.getElementById('skills-list');
|
|
318
|
+
root.innerHTML = '<div class="empty">Loading…</div>';
|
|
319
|
+
try {
|
|
320
|
+
const r = await api('/skills');
|
|
321
|
+
const arr = r.skills || r;
|
|
322
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
323
|
+
root.innerHTML = '<div class="empty">No skills yet. Install one: <code>lazyclaw skills install <user>/<repo></code>.</div>';
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
root.innerHTML = '';
|
|
327
|
+
arr.forEach((s) => {
|
|
328
|
+
const div = document.createElement('div');
|
|
329
|
+
div.className = 'card';
|
|
330
|
+
div.innerHTML = `<div class="row" style="border:0;padding:0;">
|
|
331
|
+
<div class="name">${s.name}</div>
|
|
332
|
+
<div class="dim" style="margin-left:auto;">${(s.bytes ?? '')} bytes</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="dim" style="margin-top:6px;">${s.summary || ''}</div>`;
|
|
335
|
+
root.appendChild(div);
|
|
336
|
+
});
|
|
337
|
+
} catch (e) {
|
|
338
|
+
root.innerHTML = `<div class="empty">⚠ ${e.message}</div>`;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
LOADERS.providers = async function loadProviders() {
|
|
343
|
+
const root = document.getElementById('providers-list');
|
|
344
|
+
root.innerHTML = '<div class="empty">Loading…</div>';
|
|
345
|
+
try {
|
|
346
|
+
const r = await api('/providers');
|
|
347
|
+
const arr = r.providers || r;
|
|
348
|
+
root.innerHTML = '';
|
|
349
|
+
arr.forEach((p) => {
|
|
350
|
+
const div = document.createElement('div');
|
|
351
|
+
div.className = 'card';
|
|
352
|
+
const tag = p.requiresApiKey
|
|
353
|
+
? '<span class="pill warn">api key</span>'
|
|
354
|
+
: '<span class="pill ok">no key</span>';
|
|
355
|
+
const models = (p.suggestedModels || []).slice(0, 6).join(' · ') || '<span class="dim">(default)</span>';
|
|
356
|
+
div.innerHTML = `<div class="row" style="border:0;padding:0;">
|
|
357
|
+
<div class="name">${p.name}</div>${tag}
|
|
358
|
+
<div class="dim" style="margin-left:auto;">${p.endpoint || ''}</div>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="dim" style="margin-top:6px;">${p.docs || ''}</div>
|
|
361
|
+
<div style="margin-top:8px;font-size:12px;">${models}</div>`;
|
|
362
|
+
root.appendChild(div);
|
|
363
|
+
});
|
|
364
|
+
} catch (e) {
|
|
365
|
+
root.innerHTML = `<div class="empty">⚠ ${e.message}</div>`;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
LOADERS.status = async function loadStatus() {
|
|
370
|
+
const root = document.getElementById('status-card');
|
|
371
|
+
root.innerHTML = '<div class="empty">Loading…</div>';
|
|
372
|
+
try {
|
|
373
|
+
const r = await api('/status');
|
|
374
|
+
root.innerHTML = `<div class="card"><pre>${JSON.stringify(r, null, 2)}</pre></div>`;
|
|
375
|
+
} catch (e) {
|
|
376
|
+
root.innerHTML = `<div class="empty">⚠ ${e.message}</div>`;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// First load = chat tab.
|
|
381
|
+
LOADERS.chat();
|
|
382
|
+
|
|
383
|
+
// ── Chat send ─────────────────────────────────────────────────
|
|
384
|
+
let chatHistory = []; // [{role, text}]
|
|
385
|
+
function resetChat() {
|
|
386
|
+
chatHistory = [];
|
|
387
|
+
const stream = document.getElementById('chat-stream');
|
|
388
|
+
stream.innerHTML = '<div class="empty">Type below to start.</div>';
|
|
389
|
+
document.getElementById('chat-meta').textContent = '';
|
|
390
|
+
}
|
|
391
|
+
function appendMsg(role, text) {
|
|
392
|
+
const stream = document.getElementById('chat-stream');
|
|
393
|
+
// First message kicks the empty placeholder.
|
|
394
|
+
if (stream.querySelector('.empty')) stream.innerHTML = '';
|
|
395
|
+
const div = document.createElement('div');
|
|
396
|
+
div.className = 'msg ' + role;
|
|
397
|
+
div.textContent = text;
|
|
398
|
+
stream.appendChild(div);
|
|
399
|
+
stream.scrollTop = stream.scrollHeight;
|
|
400
|
+
return div;
|
|
401
|
+
}
|
|
402
|
+
document.getElementById('chat-input').addEventListener('keydown', (e) => {
|
|
403
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
sendChat();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
async function sendChat() {
|
|
409
|
+
const ta = document.getElementById('chat-input');
|
|
410
|
+
const text = ta.value.trim();
|
|
411
|
+
if (!text) return;
|
|
412
|
+
const assignee = document.getElementById('chat-assignee').value;
|
|
413
|
+
if (!assignee) {
|
|
414
|
+
appendMsg('error', 'No provider selected. Run `lazyclaw onboard` first.');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
ta.value = '';
|
|
418
|
+
appendMsg('user', text);
|
|
419
|
+
chatHistory.push({ role: 'user', text });
|
|
420
|
+
const meta = document.getElementById('chat-meta');
|
|
421
|
+
meta.textContent = '⏳ thinking…';
|
|
422
|
+
const t0 = Date.now();
|
|
423
|
+
try {
|
|
424
|
+
// Daemon's POST /agent: { prompt, provider, model, ... }
|
|
425
|
+
// Returns { text, provider, model, durationMs, ... }.
|
|
426
|
+
const [provName, modelName] = assignee.includes(':') ? assignee.split(':', 2) : [assignee, ''];
|
|
427
|
+
const body = { prompt: buildAgentPrompt(text), provider: provName };
|
|
428
|
+
if (modelName) body.model = modelName;
|
|
429
|
+
const r = await api('/agent', {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: { 'content-type': 'application/json' },
|
|
432
|
+
body: JSON.stringify(body),
|
|
433
|
+
});
|
|
434
|
+
const reply = r.text || r.output || '(empty)';
|
|
435
|
+
appendMsg('assistant', reply);
|
|
436
|
+
chatHistory.push({ role: 'assistant', text: reply });
|
|
437
|
+
const dur = ((Date.now() - t0) / 1000).toFixed(1);
|
|
438
|
+
meta.textContent = `${r.provider || provName} · ${r.model || modelName || '(default)'} · ${dur}s`;
|
|
439
|
+
} catch (e) {
|
|
440
|
+
appendMsg('error', '⚠ ' + (e.message || String(e)));
|
|
441
|
+
meta.textContent = '';
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function buildAgentPrompt(latestUserText) {
|
|
445
|
+
// Flat conversation prompt: previous turns + the new user message.
|
|
446
|
+
// The daemon's /agent endpoint is one-shot, so we stuff prior
|
|
447
|
+
// turns into the prompt body. Keeps the dashboard stateless.
|
|
448
|
+
if (chatHistory.length <= 1) return latestUserText;
|
|
449
|
+
const lines = [];
|
|
450
|
+
for (const m of chatHistory.slice(-12, -1)) {
|
|
451
|
+
lines.push((m.role === 'user' ? 'User:' : 'Assistant:') + ' ' + m.text);
|
|
452
|
+
}
|
|
453
|
+
lines.push('User: ' + latestUserText);
|
|
454
|
+
lines.push('Assistant:');
|
|
455
|
+
return lines.join('\n\n');
|
|
456
|
+
}
|
|
457
|
+
</script>
|
|
458
|
+
</body>
|
|
459
|
+
</html>
|