agentgui 1.0.944 → 1.0.945
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/AGENTS.md +7 -5
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/site/app/index.html +6 -2
- package/site/app/js/app.js +77 -121
package/AGENTS.md
CHANGED
|
@@ -6,9 +6,11 @@ One surface. `server.js` serves `site/app/` under `BASE_URL` (default `/gm`) and
|
|
|
6
6
|
|
|
7
7
|
When `PASSWORD` env var is set, every HTTP route is gated by `lib/http-handler.js` accepting **Basic auth**, **`Authorization: Bearer <pwd>`**, OR **`?token=<pwd>`** query param (added 2026-05-26 so `EventSource` and direct deep-links work — neither can set headers). WS `/sync` requires `?token=` only. The HTML head script injects `window.__BASE_URL`, `window.__SERVER_VERSION`, and `window.__WS_TOKEN`; `site/app/js/backend.js` reads `__WS_TOKEN` and threads it onto every fetch (Bearer header) / EventSource (qs) / WebSocket (qs).
|
|
8
8
|
|
|
9
|
-
- `site/app/index.html` — shell + CSS
|
|
10
|
-
- `site/app/js/backend.js` — same-origin client (`DEFAULT_BACKEND = ''`); `?backend=` query override for cross-origin debugging
|
|
11
|
-
- `site/app/js/app.js` — webjsx view + state
|
|
9
|
+
- `site/app/index.html` — shell + CSS; imports `anentrypoint-design` from `https://unpkg.com/anentrypoint-design@latest` at runtime (always-latest; requires network — the prior offline guarantee is intentionally dropped)
|
|
10
|
+
- `site/app/js/backend.js` — same-origin WS/HTTP client (`DEFAULT_BACKEND = ''`); the transport glue that wires the kit; `?backend=` query override for cross-origin debugging
|
|
11
|
+
- `site/app/js/app.js` — webjsx view + state; renders the `AgentChat` kit (from `anentrypoint-design`) for the chat surface and wires agentgui's WS/ccsniff state as kit callbacks; history/settings remain agentgui-local. Exposes `window.__agentgui`
|
|
12
|
+
|
|
13
|
+
The chat GUI lives in the design kit, not in agentgui. The reusable multi-agent chat surface is the `AgentChat` component in `anentrypoint-design` (`src/components/agent-chat.js`); agentgui keeps only the transport glue (WS `backend.js`, ccsniff history wiring, agent orchestration) and passes state + callbacks into the kit. To change chat UI, edit the kit and push it (CI publishes to npm → unpkg `@latest`), not agentgui.
|
|
12
14
|
- `server.js` — boots ACP/agents/websocket plugins, mounts `createHistoryRouter()` from `ccsniff` at `/`, serves `site/app/` as static root
|
|
13
15
|
- Plugins kept (lib/plugins/): acp, agents, database, files, stream, websocket, workflow
|
|
14
16
|
|
|
@@ -145,9 +147,9 @@ Throttle renders via `requestAnimationFrame` to avoid event storm during burst l
|
|
|
145
147
|
|
|
146
148
|
The endpoints are served by ccsniff's Express router mounted in-process from `server.js`. No external proxy.
|
|
147
149
|
|
|
148
|
-
##
|
|
150
|
+
## Runtime CDN: GUI loaded from unpkg (2026-05-29)
|
|
149
151
|
|
|
150
|
-
The GUI
|
|
152
|
+
The GUI loads `anentrypoint-design` from `https://unpkg.com/anentrypoint-design@latest` at runtime (import map + CSS link in `index.html`) so agentgui always tracks the latest published kit with no hand-maintained vendored copy. This **requires a network connection** — the earlier offline guarantee was intentionally dropped in favour of always-latest. The DS itself fetches its fonts from `unpkg.com/anentrypoint-design@latest/dist/fonts/` (set in the kit's `scripts/build.mjs` `FONT_BASE`). The `site/app/vendor/` tree is no longer imported; the old vendored DS bundle + `vendor/cdn/` assets are dead and may be removed. Witness the live GUI by confirming the import map resolves to unpkg and `.agentchat` renders with 0 console errors.
|
|
151
153
|
|
|
152
154
|
## Agent/model/session management (2026-05-28)
|
|
153
155
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [Unreleased] - GUI moved into the anentrypoint-design kit, loaded from unpkg
|
|
2
|
+
|
|
3
|
+
- the chat GUI now lives in the design kit: added a reusable `AgentChat` component to anentrypoint-design (agent-then-model picker + AICat auto-scroll/thinking + tool parts + cwd bar + caret-stable composer), pure props-in/vnode-out with all transport as host callbacks. Published via the kit's CI to npm -> unpkg.
|
|
4
|
+
- site/app/index.html: imports anentrypoint-design from https://unpkg.com/anentrypoint-design@latest at runtime (always-latest), replacing the vendored bundle. Trades the offline guarantee for always-latest, per project decision.
|
|
5
|
+
- site/app/js/app.js: chatMain renders the AgentChat kit and wires agentgui state + WS/ccsniff glue as callbacks; the agent/model picker, cwd bar, and composer moved out of agentgui into the kit. backend.js (WS client) stays as the transport glue.
|
|
6
|
+
- verified end-to-end in a real browser: GUI sourced from unpkg, AgentChat renders, chat round-trips (reply streams into the kit's transcript), 0 console errors.
|
|
7
|
+
|
|
1
8
|
## [Unreleased] - live e2e chat verified + composer caret fix + ccsniff memory cap
|
|
2
9
|
|
|
3
10
|
- verified the full GUI chat round-trip end-to-end in a real browser: compose -> WS chat.sendMessage -> claude-code -> streaming_progress -> transcript renders the streamed reply (not just the runner harness).
|
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<title>agentgui</title>
|
|
7
7
|
<meta name="description" content="agentgui — multi-agent client with same-origin server, in-process ccsniff history, and ACP chat.">
|
|
8
|
-
|
|
8
|
+
<!-- The GUI lives in the anentrypoint-design kit and is loaded from unpkg at
|
|
9
|
+
its latest published version, so agentgui always tracks the kit without a
|
|
10
|
+
hand-maintained vendored copy. (This trades the prior offline guarantee for
|
|
11
|
+
always-latest, per project decision; a network connection is required.) -->
|
|
12
|
+
<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
|
|
9
13
|
<script type="importmap">
|
|
10
|
-
{ "imports": { "anentrypoint-design": "
|
|
14
|
+
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
|
|
11
15
|
</script>
|
|
12
16
|
<style>
|
|
13
17
|
:root {
|
package/site/app/js/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList, Spinner, Alert } = C;
|
|
6
|
+
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList, Spinner, Alert } = C;
|
|
7
7
|
|
|
8
8
|
const state = {
|
|
9
9
|
backend: B.getBackend(),
|
|
@@ -135,6 +135,27 @@ function selectModel(id) {
|
|
|
135
135
|
function agentById(id) { return state.agents.find(a => a.id === id); }
|
|
136
136
|
function agentAvailable(id) { const a = agentById(id); return !a || a.available !== false; }
|
|
137
137
|
|
|
138
|
+
// The four flagship orchestration targets surface first, then other available
|
|
139
|
+
// agents, then npx-installable, then not-installed — so the agents the GUI
|
|
140
|
+
// exists to drive are reachable without scanning a flat 17-item list. This
|
|
141
|
+
// ordering is agentgui's orchestration priority, so it stays in the host and is
|
|
142
|
+
// passed pre-sorted to the (app-agnostic) AgentChat kit.
|
|
143
|
+
const PRIMARY_AGENTS = ['claude-code', 'opencode', 'kilo', 'agy'];
|
|
144
|
+
function sortedAgents() {
|
|
145
|
+
const rank = (a) => {
|
|
146
|
+
const primary = PRIMARY_AGENTS.indexOf(a.id);
|
|
147
|
+
const avail = a.available !== false;
|
|
148
|
+
if (primary !== -1 && avail) return primary;
|
|
149
|
+
if (avail) return 10;
|
|
150
|
+
if (a.npxInstallable) return 20;
|
|
151
|
+
return 30;
|
|
152
|
+
};
|
|
153
|
+
return state.agents
|
|
154
|
+
.map(a => ({ a, rank: rank(a) }))
|
|
155
|
+
.sort((x, y) => x.rank - y.rank || x.a.name.localeCompare(y.a.name))
|
|
156
|
+
.map(({ a }) => a);
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
function navTo(tab) {
|
|
139
160
|
const prev = state.tab;
|
|
140
161
|
state.tab = tab;
|
|
@@ -287,55 +308,9 @@ function view() {
|
|
|
287
308
|
onNav: (label) => navTo(label),
|
|
288
309
|
});
|
|
289
310
|
|
|
290
|
-
// The
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
// the DS Select renders a plain option list.
|
|
294
|
-
const PRIMARY_AGENTS = ['claude-code', 'opencode', 'kilo', 'agy'];
|
|
295
|
-
const agentRank = (a) => {
|
|
296
|
-
const primary = PRIMARY_AGENTS.indexOf(a.id);
|
|
297
|
-
const avail = a.available !== false;
|
|
298
|
-
if (primary !== -1 && avail) return primary; // 0..3 flagship + available
|
|
299
|
-
if (avail) return 10; // other available
|
|
300
|
-
if (a.npxInstallable) return 20; // installable via npx
|
|
301
|
-
return 30; // not installed
|
|
302
|
-
};
|
|
303
|
-
const agentOptions = state.agents
|
|
304
|
-
.map(a => ({ a, rank: agentRank(a) }))
|
|
305
|
-
.sort((x, y) => x.rank - y.rank || x.a.name.localeCompare(y.a.name))
|
|
306
|
-
.map(({ a }) => ({
|
|
307
|
-
value: a.id,
|
|
308
|
-
label: a.name + (a.available === false ? (a.npxInstallable ? ' (via npx)' : ' (not installed)') : ''),
|
|
309
|
-
disabled: a.available === false && !a.npxInstallable,
|
|
310
|
-
}));
|
|
311
|
-
const showModelPicker = state.tab === 'chat' && state.agentModels.length > 0;
|
|
312
|
-
|
|
313
|
-
const crumbRight = state.tab === 'chat'
|
|
314
|
-
? [h('div', { key: 'cc', class: 'chat-controls' },
|
|
315
|
-
Select({
|
|
316
|
-
key: 'agentsel',
|
|
317
|
-
value: state.selectedAgent,
|
|
318
|
-
placeholder: '— agent —',
|
|
319
|
-
title: 'Select coding agent',
|
|
320
|
-
options: agentOptions,
|
|
321
|
-
onChange: (v) => { selectAgent(v); },
|
|
322
|
-
}),
|
|
323
|
-
showModelPicker
|
|
324
|
-
? Select({
|
|
325
|
-
key: 'modelsel',
|
|
326
|
-
value: state.selectedModel,
|
|
327
|
-
placeholder: '— model —',
|
|
328
|
-
title: 'Select model for this agent',
|
|
329
|
-
options: state.agentModels.map(m => ({ value: m.id, label: m.name || m.id })),
|
|
330
|
-
onChange: (v) => { selectModel(v); },
|
|
331
|
-
})
|
|
332
|
-
: null,
|
|
333
|
-
state.chat.busy
|
|
334
|
-
? Btn({ key: 'stop', onClick: cancelChat, children: 'stop', title: 'Stop streaming' })
|
|
335
|
-
: Btn({ key: 'new', onClick: newChat, children: '+ new', title: 'Start new chat (clears history)' }),
|
|
336
|
-
dot,
|
|
337
|
-
)]
|
|
338
|
-
: [dot];
|
|
311
|
+
// The agent/model picker + new/stop now live inside the AgentChat kit (chat
|
|
312
|
+
// tab), so the crumb carries only the status dot on every tab.
|
|
313
|
+
const crumbRight = [dot];
|
|
339
314
|
|
|
340
315
|
// Topbar already shows "agentgui / <tab>"; the crumb is reserved for contextual
|
|
341
316
|
// controls (model picker, new/stop, live status) so it doesn't duplicate the path.
|
|
@@ -407,46 +382,11 @@ function errText(e) {
|
|
|
407
382
|
}
|
|
408
383
|
|
|
409
384
|
function chatMain() {
|
|
410
|
-
const lastIdx = state.chat.messages.length - 1;
|
|
411
385
|
const agentName = agentById(state.selectedAgent)?.name || state.selectedAgent || 'agent';
|
|
412
|
-
const msgs = state.chat.messages.map((m, i) => {
|
|
413
|
-
const isAssistant = m.role === 'assistant';
|
|
414
|
-
const isStreaming = state.chat.busy && i === lastIdx && isAssistant;
|
|
415
|
-
const hasParts = Array.isArray(m.parts) && m.parts.length > 0;
|
|
416
|
-
const isEmptyStreaming = isStreaming && !m.content && !hasParts;
|
|
417
|
-
const parts = [];
|
|
418
|
-
if (m.content) parts.push({ kind: isAssistant ? 'md' : 'text', text: m.content });
|
|
419
|
-
if (hasParts) for (const p of m.parts) parts.push({ kind: 'text', text: p });
|
|
420
|
-
return {
|
|
421
|
-
key: m.id || String(i),
|
|
422
|
-
who: isAssistant ? 'them' : 'you',
|
|
423
|
-
name: isAssistant ? agentName : 'you',
|
|
424
|
-
time: m.time || '',
|
|
425
|
-
typing: isEmptyStreaming,
|
|
426
|
-
parts: isEmptyStreaming ? undefined : (parts.length ? parts : [{ kind: 'text', text: '' }]),
|
|
427
|
-
};
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const placeholder = !state.selectedAgent
|
|
431
|
-
? 'choose an agent first'
|
|
432
|
-
: (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message…');
|
|
433
|
-
const composer = ChatComposer({
|
|
434
|
-
value: state.chat.draft,
|
|
435
|
-
disabled: !canSend(),
|
|
436
|
-
placeholder,
|
|
437
|
-
// The DS textarea is controlled and reads its live DOM value on send, so we
|
|
438
|
-
// do NOT re-render on every keystroke (that fights the cursor and is wasteful).
|
|
439
|
-
// Re-render only when the draft crosses empty<->non-empty, since that is the
|
|
440
|
-
// only transition that changes the send button's disabled state.
|
|
441
|
-
onInput: (v) => {
|
|
442
|
-
const was = !!(state.chat.draft && state.chat.draft.trim());
|
|
443
|
-
const now = !!(v && v.trim());
|
|
444
|
-
state.chat.draft = v;
|
|
445
|
-
if (was !== now) render();
|
|
446
|
-
},
|
|
447
|
-
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
448
|
-
});
|
|
449
386
|
|
|
387
|
+
// Banners agentgui owns (resume, agent-switched, unavailable, confirm-clear,
|
|
388
|
+
// stream error) are pre-built here and handed to the kit, which renders them
|
|
389
|
+
// above the thread. The kit holds no agentgui-specific banner logic.
|
|
450
390
|
const banners = [];
|
|
451
391
|
if (state.chat.resumeSid) {
|
|
452
392
|
banners.push(h('div', { key: 'rb', class: 'resume-banner', role: 'status' },
|
|
@@ -456,7 +396,6 @@ function chatMain() {
|
|
|
456
396
|
banners.push(Alert({ key: 'rnote', kind: 'info', title: 'Agent switched', children: state.chat.resumeNote }));
|
|
457
397
|
}
|
|
458
398
|
}
|
|
459
|
-
banners.push(cwdBanner());
|
|
460
399
|
if (state.selectedAgent && !agentAvailable(state.selectedAgent)) {
|
|
461
400
|
banners.push(Alert({ key: 'unavail', kind: 'warn', title: agentName + ' is not installed',
|
|
462
401
|
children: 'This agent\'s CLI was not found on the server. Pick another agent or install it.' }));
|
|
@@ -468,26 +407,59 @@ function chatMain() {
|
|
|
468
407
|
Btn({ key: 'cnyes', danger: true, onClick: newChat, children: 'clear' }),
|
|
469
408
|
Btn({ key: 'cnno', onClick: () => { state.confirmingNewChat = false; render(); }, children: 'cancel' })] }));
|
|
470
409
|
}
|
|
471
|
-
// Last stream error surfaced as a proper Alert instead of raw JSON in the bubble.
|
|
472
410
|
const lastErr = state.chat.messages.length ? state.chat.messages[state.chat.messages.length - 1].error : null;
|
|
473
411
|
if (lastErr && !state.chat.busy) {
|
|
474
412
|
banners.push(Alert({ key: 'chaterr', kind: 'error', title: 'Stream error', children: lastErr }));
|
|
475
413
|
}
|
|
414
|
+
|
|
415
|
+
const placeholder = !state.selectedAgent
|
|
416
|
+
? 'choose an agent first'
|
|
417
|
+
: (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message…');
|
|
418
|
+
|
|
419
|
+
// The reusable AgentChat kit owns the agent/model picker, cwd bar, transcript
|
|
420
|
+
// (with AICat-style auto-scroll + thinking), and the caret-stable composer.
|
|
421
|
+
// agentgui supplies state and wires every server interaction as a callback.
|
|
476
422
|
return [
|
|
477
423
|
offlineBanner(),
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
424
|
+
AgentChat({
|
|
425
|
+
agents: sortedAgents(),
|
|
426
|
+
selectedAgent: state.selectedAgent,
|
|
427
|
+
models: state.agentModels,
|
|
428
|
+
selectedModel: state.selectedModel,
|
|
429
|
+
messages: state.chat.messages,
|
|
430
|
+
busy: state.chat.busy,
|
|
431
|
+
draft: state.chat.draft,
|
|
432
|
+
status: state.chat.busy ? 'streaming…' : (state.chat.resumeSid ? 'resume' : 'ready'),
|
|
433
|
+
agentName,
|
|
434
|
+
placeholder,
|
|
435
|
+
canSend: canSend(),
|
|
436
|
+
banners,
|
|
437
|
+
cwd: state.chatCwd,
|
|
438
|
+
cwdEditing: !!state.cwdEditing,
|
|
439
|
+
cwdDraft: state.cwdDraft,
|
|
440
|
+
onSelectAgent: (v) => selectAgent(v),
|
|
441
|
+
onSelectModel: (v) => selectModel(v),
|
|
442
|
+
onNewChat: newChat,
|
|
443
|
+
onStop: cancelChat,
|
|
444
|
+
onInput: (v) => {
|
|
445
|
+
// The kit's textarea is controlled and reads its live DOM value on send,
|
|
446
|
+
// so re-render only when the draft crosses empty<->non-empty (the only
|
|
447
|
+
// transition that toggles the send button's disabled state).
|
|
448
|
+
const was = !!(state.chat.draft && state.chat.draft.trim());
|
|
449
|
+
const now = !!(v && v.trim());
|
|
450
|
+
state.chat.draft = v;
|
|
451
|
+
if (was !== now) render();
|
|
452
|
+
},
|
|
453
|
+
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
454
|
+
onCwdEdit: () => { state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; render(); },
|
|
455
|
+
onCwdSave: () => {
|
|
456
|
+
state.chatCwd = (state.cwdDraft ?? '').trim();
|
|
457
|
+
if (state.chatCwd) lsSet('agentgui.cwd', state.chatCwd); else lsRemove('agentgui.cwd');
|
|
458
|
+
state.cwdEditing = false; state.cwdDraft = undefined; render();
|
|
459
|
+
},
|
|
460
|
+
onCwdCancel: () => { state.cwdEditing = false; state.cwdDraft = undefined; render(); },
|
|
461
|
+
onCwdClear: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); },
|
|
462
|
+
onCwdDraft: (v) => { state.cwdDraft = v; },
|
|
491
463
|
}),
|
|
492
464
|
].filter(Boolean);
|
|
493
465
|
}
|
|
@@ -498,24 +470,8 @@ function offlineBanner() {
|
|
|
498
470
|
children: 'agentgui can\'t reach the server (' + (state.health.error || state.health.status) + '). Chat and history actions will fail until it reconnects.' });
|
|
499
471
|
}
|
|
500
472
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return h('div', { key: 'cwdb', class: 'resume-banner', role: 'group', 'aria-label': 'Set working directory' },
|
|
504
|
-
TextField({ key: 'cwdfield', label: 'working directory (blank = server default)', value: state.cwdDraft ?? state.chatCwd ?? '',
|
|
505
|
-
placeholder: 'absolute path', onInput: (v) => { state.cwdDraft = v; } }),
|
|
506
|
-
Btn({ key: 'cwdsave', primary: true, onClick: () => {
|
|
507
|
-
state.chatCwd = (state.cwdDraft ?? '').trim();
|
|
508
|
-
if (state.chatCwd) lsSet('agentgui.cwd', state.chatCwd); else lsRemove('agentgui.cwd');
|
|
509
|
-
state.cwdEditing = false; state.cwdDraft = undefined; render();
|
|
510
|
-
}, children: 'save' }),
|
|
511
|
-
Btn({ key: 'cwdcancel', onClick: () => { state.cwdEditing = false; state.cwdDraft = undefined; render(); }, children: 'cancel' }));
|
|
512
|
-
}
|
|
513
|
-
return h('div', { key: 'cwdb', class: 'cwd-bar', role: 'group', 'aria-label': 'Working directory' },
|
|
514
|
-
h('span', { key: 'cwdtxt', class: 'lede cwd-bar-text', title: state.chatCwd || 'server default working directory' },
|
|
515
|
-
state.chatCwd ? 'cwd: ' + truncate(state.chatCwd, 28, 60) : 'cwd: server default'),
|
|
516
|
-
h('button', { key: 'cwdset', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; render(); } }, state.chatCwd ? 'change' : 'set'),
|
|
517
|
-
state.chatCwd ? h('button', { key: 'cwdclr', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); } }, 'use default') : null);
|
|
518
|
-
}
|
|
473
|
+
// (The working-directory bar now lives in the AgentChat kit; agentgui wires its
|
|
474
|
+
// cwd state + handlers as kit callbacks in chatMain.)
|
|
519
475
|
|
|
520
476
|
function newChat() {
|
|
521
477
|
if (state.chat.messages.length && !state.confirmingNewChat) {
|