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 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, imports `anentrypoint-design` from unpkg
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, kits-only rendering (PageHeader, SearchInput, TextField, EventList, Panel, Row, Section); exposes `window.__agentgui`
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
- ## Zero-runtime-CDN: vendored DS deps (2026-05-28)
150
+ ## Runtime CDN: GUI loaded from unpkg (2026-05-29)
149
151
 
150
- The GUI runs fully offline. `site/app/vendor/cdn/` holds marked, dompurify, prismjs components, and JetBrains+Space Grotesk woff2. The DS bundle (`anentrypoint-design/247420.js`) had its CDN string constants rewritten to local paths, AND its gzip+base64-embedded CSS blob (`Ha`) regenerated to point at local fonts. Full mechanism is in rs-learn (recall "DS bundle gzip Ha vendoring"). Witness offline by browser route-abort of non-origin hosts.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.944",
3
+ "version": "1.0.945",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -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
- <link rel="stylesheet" href="vendor/anentrypoint-design/247420.css">
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": "./vendor/anentrypoint-design/247420.js" } }
14
+ { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
11
15
  </script>
12
16
  <style>
13
17
  :root {
@@ -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 four flagship orchestration targets surface first, then the rest of the
291
- // available agents, then unavailable ones so the agents the GUI exists to
292
- // drive are reachable without scanning a flat 17-item list. Ordering only;
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
- ...banners,
479
- Chat({
480
- title: agentName + (state.selectedModel ? ' · ' + state.selectedModel : '') + (state.chat.resumeSid ? ' · resume' : ''),
481
- // Explicit human-readable sub; the DS default ("NN msgs", zero-padded)
482
- // leaks an event-list style into chat. Hide it when there are no messages
483
- // (the empty-state already says "no messages yet").
484
- sub: state.chat.busy
485
- ? 'streaming…'
486
- : (state.chat.messages.length
487
- ? state.chat.messages.length + (state.chat.messages.length === 1 ? ' message' : ' messages')
488
- : ''),
489
- messages: msgs,
490
- composer,
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
- function cwdBanner() {
502
- if (state.cwdEditing) {
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) {