agentgui 1.0.948 → 1.0.949

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.
@@ -0,0 +1,86 @@
1
+ export const meta = {
2
+ name: 'gui-audit',
3
+ description: 'Sweep every agentgui GUI surface for jank, verify findings adversarially, and emit kit-update tasks',
4
+ whenToUse: 'Run a maximum-effort GUI audit of agentgui: review chat, history, settings, a11y, security, performance, and glyph hygiene across the client (site/app) and the design kit (c:\\dev\\anentrypoint-design). Fans out one reviewer per surface, verifies each finding with an independent skeptic, then synthesizes a prioritized punch-list. GUI fixes land in the design kit.',
5
+ phases: [
6
+ { title: 'Review', detail: 'one reviewer agent per GUI surface/aspect' },
7
+ { title: 'Verify', detail: 'independent skeptic confirms each finding is real' },
8
+ { title: 'Synthesize', detail: 'dedupe + prioritize into a punch-list with kit-vs-app split' },
9
+ ],
10
+ };
11
+
12
+ // Every GUI surface and cross-cutting aspect the audit must cover. Each becomes
13
+ // one reviewer agent. Add a row to widen coverage; the fan-out scales with it.
14
+ const SURFACES = [
15
+ { key: 'chat', prompt: 'Audit the agentgui CHAT surface for jank. Read site/app/js/app.js (chatMain, sendChat, toolPart, applyToolResult), site/app/js/backend.js (streamChat), and the kit AgentChat at c:\\dev\\anentrypoint-design/src/components/agent-chat.js + chat.js. Check: tool_use/tool_result render as structured collapsible cards (not flattened text); markdown/text/tool parts interleave in arrival order; empty state; composer stop button while streaming; thinking/working indicator during silent tool runs; aborted-empty turn pruning; multi-turn resume correctness. Report each issue with file:line, why it is jank, and whether the fix is in the kit or the app.' },
16
+ { key: 'history', prompt: 'Audit the agentgui HISTORY surface (site/app/js/app.js historyMain, historySide, loadSession, EventList usage). Check: event rendering consistency with chat (same event same way), expand/collapse + keyboard activation, the silent 300-event cap with no load-older, search-result click landing on the matched event, live SSE wiring, session list filtering/subagent toggle. Report each issue with file:line and kit-vs-app.' },
17
+ { key: 'settings', prompt: 'Audit the agentgui SETTINGS surface (site/app/js/app.js settingsMain, agentsPanel, preferencesPanel, saveBackend). Check: ACP agent start/stop/restart/health controls (read-only today), cwd surfacing + server-side validation, backend URL validation, clear-local-data flow, health summary clarity. Report each issue with file:line and kit-vs-app.' },
18
+ { key: 'a11y', prompt: 'Audit agentgui ACCESSIBILITY across site/app and the kit. Check: skip-link target reachability/focus, tab order on each tab, agent/model select labels, EventList row role=button keyboard activation + expand announcement, aria-live over/under-announcing, color contrast on the dark theme (WCAG AA 4.5:1 text / 3:1 UI), focus-visible rings, reduced-motion. Report each issue with file:line.' },
19
+ { key: 'security', prompt: 'Audit agentgui GUI-reachable SECURITY. Read lib/http-handler.js. Check: Access-Control-Allow-Origin:* with Authorization + ?token= cross-origin/leak risk; /api/image arbitrary-file-read (the .. check runs after path.normalize); markdown XSS (kit marked+dompurify) — does DOMPurify actually neutralize script/javascript: URLs in agent output. Report each issue with file:line, severity, and the concrete exploit path.' },
20
+ { key: 'perf', prompt: 'Audit agentgui GUI PERFORMANCE. Check: per-token render+scroll thrash on the chat stream (should be rAF-coalesced via scheduleStreamRender), history EventList rebuild cost on burst SSE loads (last-300 map per rAF), webjsx keying stability, markdown/Prism cache warmup, resize/relTime tick cost. Report each hotspot with file:line and a concrete fix.' },
21
+ { key: 'glyph', prompt: 'Sweep EVERY GUI source file (site/app/**, c:\\dev\\anentrypoint-design/src/**, *.css) for decorative tell-tale glyphs: arrows, box-drawing, bullets, status dots, checks, emoji, any non-ASCII decorative symbol. KEEP the typographic set (middot, ellipsis, em/en-dash in prose), SVG line-icons via Icon(), CSS-drawn discs, and intentional ascii-art (AICAT_FACE). Report every decorative glyph found with file:line and its ASCII replacement.' },
22
+ { key: 'responsive', prompt: 'Audit agentgui RESPONSIVE behavior from site/app/index.html media queries + the kit AppShell. Check: chat controls cluster wrap at 360/640/900px, cwd path overflow, history sidebar drawer (open/close/scrim/select-closes-it), touch-target sizes, settings two-column collapse. Report each issue with the breakpoint and file:line.' },
23
+ { key: 'consistency', prompt: 'Audit agentgui PREDICTABILITY/CONSISTENCY. Check that the same state reads the same way everywhere: connection/activity status vocabulary across crumb dot, chat status, AgentControls status, settings health, agentsPanel; ellipsis convention (… vs ...); rail tone semantics (green=ok/selected, purple=subagent, flame=error); button labels. Report each divergence with file:line.' },
24
+ ];
25
+
26
+ const FINDINGS_SCHEMA = {
27
+ type: 'object',
28
+ required: ['surface', 'findings'],
29
+ properties: {
30
+ surface: { type: 'string' },
31
+ findings: {
32
+ type: 'array',
33
+ items: {
34
+ type: 'object',
35
+ required: ['title', 'where', 'why', 'fixLocation'],
36
+ properties: {
37
+ title: { type: 'string' },
38
+ where: { type: 'string', description: 'file:line' },
39
+ why: { type: 'string' },
40
+ fixLocation: { type: 'string', enum: ['kit', 'app', 'server', 'both'] },
41
+ severity: { type: 'string', enum: ['low', 'medium', 'high'] },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ };
47
+
48
+ const VERDICT_SCHEMA = {
49
+ type: 'object',
50
+ required: ['isReal', 'reason'],
51
+ properties: {
52
+ isReal: { type: 'boolean' },
53
+ reason: { type: 'string' },
54
+ correctedFix: { type: 'string' },
55
+ },
56
+ };
57
+
58
+ phase('Review');
59
+
60
+ // Pipeline: each surface is reviewed, then every finding it produced is
61
+ // independently verified — a surface's findings start verifying the moment that
62
+ // surface's review returns, without waiting for slower surfaces.
63
+ const reviewed = await pipeline(
64
+ SURFACES,
65
+ (s) => agent(s.prompt, { label: 'review:' + s.key, phase: 'Review', schema: FINDINGS_SCHEMA }),
66
+ (review, surface) =>
67
+ parallel((review.findings || []).map((f) => () =>
68
+ agent(
69
+ 'Adversarially verify this GUI audit finding. Default to isReal=false if you cannot confirm it from the actual source. Finding: "' + f.title + '" at ' + f.where + ' — ' + f.why,
70
+ { label: 'verify:' + surface.key, phase: 'Verify', schema: VERDICT_SCHEMA }
71
+ ).then((v) => ({ ...f, surface: surface.key, verdict: v }))
72
+ ))
73
+ );
74
+
75
+ const confirmed = reviewed.flat().filter(Boolean).filter((f) => f.verdict && f.verdict.isReal);
76
+ log(confirmed.length + ' confirmed findings across ' + SURFACES.length + ' surfaces');
77
+
78
+ phase('Synthesize');
79
+
80
+ const punchList = await agent(
81
+ 'Synthesize these confirmed agentgui GUI audit findings into a prioritized punch-list. Group by fixLocation (kit vs app vs server), order by severity then user impact, dedupe overlaps, and for each give a one-line concrete remedy. Findings JSON:\n' +
82
+ JSON.stringify(confirmed, null, 2),
83
+ { label: 'synthesize', phase: 'Synthesize' }
84
+ );
85
+
86
+ return { confirmedCount: confirmed.length, confirmed, punchList };
package/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ 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 `https://unpkg.com/anentrypoint-design@latest` at runtime (always-latest; requires network — the prior offline guarantee is intentionally dropped)
9
+ - `site/app/index.html` — shell + CSS; imports `anentrypoint-design` from the **local vendored copy** `./vendor/anentrypoint-design/247420.{js,css}` (re-vendored 2026-06-04 for a PREDICTABLE shipped UI that does not shift when upstream publishes, and offline operation — the markdown stack marked/dompurify/prismjs still fetches from jsdelivr on first chat render). Update flow: edit the kit at `c:\dev\anentrypoint-design`, `node scripts/build.mjs`, copy `dist/247420.{js,css}` into `site/app/vendor/anentrypoint-design/`, then publish the kit so unpkg stays in sync.
10
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
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
12
 
@@ -18,6 +18,20 @@ Dependencies:
18
18
  - `ccsniff` (>=1.1.0) — exports `createHistoryRouter({projectsDir})` mountable on Express; serves `/v1/history/{sessions,sessions/:sid/events,search,snapshot,reindex,stream}`. Reads `~/.claude/projects` (override via `CLAUDE_PROJECTS_DIR`).
19
19
  - `anentrypoint-design` (>=0.0.119) — kit library, single-file ESM from unpkg
20
20
 
21
+ ## GUI audit pass (2026-06-04) — structured chat, predictable presentation
22
+
23
+ A maximum-effort GUI sweep landed these, GUI changes in the kit `c:\dev\anentrypoint-design` (built+vendored locally), correctness/security in agentgui:
24
+
25
+ - **Structured tool cards in chat.** `app.js sendChat` now emits structured parts — `toolPart()` builds `{kind:'tool',name,label,args,status,_id}` and `applyToolResult()` matches a `tool_result` back to its `tool_use` by id (sets `result`+done/error status). The kit `AgentChat` passes structured `{kind,...}` parts straight through to `ChatMessage.renderPart` (only bare strings are wrapped as text), so the kit's collapsible `ToolCallNode` (args/result sections, status icons) renders instead of the old flattened `'-> '+text`. Full args/result live in the card (no 120/160-char truncation).
26
+ - **AgentChat kit additions** (`src/components/agent-chat.js`): empty state (`.agentchat-empty*`, title/sub/`suggestions`+`onSuggestionClick`), composer stop button while streaming (`busy`+`onCancel:onStop` into `ChatComposer`), a `working…` tail (`.agentchat-working`) when a content-bearing turn is still streaming (silent tool runs no longer read as frozen). CSS in `chat.css`.
27
+ - **Streaming render is rAF-coalesced** via `scheduleStreamRender()` (was render+scroll per token).
28
+ - **Multi-turn resume wired end-to-end.** Server `chat.sendMessage` is one-shot (new ephemeral `chat-` id per call); claude's real session id is `parsed.session_id` in the stream. `ws-handlers-util.js onEvent` now broadcasts `streaming_session{claudeSessionId}` once; `backend.js` yields `{type:'session'}`; `app.js` captures it into `state.chat.resumeSid` (claude-code only) + persists, so the next turn `--resume`s.
29
+ - **Skip link works.** Kit `AppShell` `<main id=app-main>` now has `tabindex=-1` so the skip-link moves focus, not just scrolls.
30
+ - **History:** `state.eventsLimit` (default 300, reset per session) + `load N older` control (no more silently-unreachable older events); expand-all/collapse-all; per-event rail tone (green=normal, purple=tool_use, flame=error) matching the GUI-wide rail semantics; search hits carry `{focusEventI,focusEventTs}` so a click scrolls to + flashes (`.event-flash`) the matched event; mobile drawer auto-closes on session select (`loadSession` removes `.side-open` since DS only auto-closes on `<a>` clicks).
31
+ - **Security** (`lib/http-handler.js`): `/api/image` confined to an allowlist root (`CLAUDE_PROJECTS_DIR`, home, `IMAGE_ROOTS`) — was arbitrary-file-read (the `..` check was a no-op after `path.normalize`); CORS no longer advertises `Access-Control-Allow-Origin:*` when `PASSWORD` is set (reflects `CORS_ORIGIN` if set, else same-origin only). Markdown XSS verified inert (DOMPurify strips script/onerror/`javascript:`).
32
+ - **Consistency:** unified ellipsis to `…` (kept set); settings health chip uses connection vocab (`connected/connecting/offline`) matching the crumb dot; running-chats panel now shows on chat tab too (extracted `runningPanel()`); aborted-empty turns pruned from persisted/restored chat; restored transcript reopens under its own agent (`restoredAgent`).
33
+ - **Workflow:** `.claude/workflows/gui-audit.js` — a reusable multi-agent GUI-audit workflow (per-surface reviewers -> adversarial verify -> synthesize, kit-vs-app fixLocation).
34
+
21
35
  ## Orchestration agents
22
36
 
23
37
  The four flagship agents the GUI drives are **Claude Code, OpenCode, Kilo, and Antigravity (`agy`)**; the agent picker (`site/app/js/app.js`, `PRIMARY_AGENTS`) sorts these first, then other-available, then npx-installable, then not-installed.
@@ -6,7 +6,19 @@ import * as term from './terminal.js';
6
6
 
7
7
  export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, PORT }) {
8
8
  return async function httpHandler(req, res) {
9
- res.setHeader('Access-Control-Allow-Origin', '*');
9
+ // CORS: when PASSWORD is set the server holds credentials a cross-origin
10
+ // page must not be able to spend, so we do NOT advertise a wildcard origin
11
+ // (a wildcard + a leaked Bearer/token would let any site the user visits
12
+ // drive this server). Reflect an explicitly-allowed origin (CORS_ORIGIN)
13
+ // when configured, else same-origin only. With no PASSWORD the server is
14
+ // already open, so the permissive wildcard is harmless and kept for tools.
15
+ const _corsOrigin = process.env.CORS_ORIGIN;
16
+ if (_corsOrigin) {
17
+ res.setHeader('Access-Control-Allow-Origin', _corsOrigin);
18
+ res.setHeader('Vary', 'Origin');
19
+ } else if (!process.env.PASSWORD) {
20
+ res.setHeader('Access-Control-Allow-Origin', '*');
21
+ } // else: no ACAO header -> browsers block cross-origin reads (same-origin still works)
10
22
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
11
23
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
12
24
  if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
@@ -124,7 +136,22 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
124
136
  const normalizedPath = path.normalize(expandedPath);
125
137
  const isWindows = os.platform() === 'win32';
126
138
  const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
127
- if (!isAbsolute || normalizedPath.includes('..')) { res.writeHead(403); res.end('Forbidden'); return; }
139
+ // Confine reads to an allowlist root. Without this the route is an
140
+ // arbitrary-file-read of any image-extensioned path on the host (the
141
+ // prior `includes('..')` guard is a no-op after path.normalize resolves
142
+ // the segments). Allowed roots: the Claude projects dir (history images)
143
+ // and the user home; override/add via IMAGE_ROOTS (path-separated).
144
+ const allowRoots = [
145
+ process.env.CLAUDE_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects'),
146
+ os.homedir(),
147
+ ...(process.env.IMAGE_ROOTS ? process.env.IMAGE_ROOTS.split(path.delimiter) : []),
148
+ ].map(r => path.normalize(r));
149
+ const norm = isWindows ? normalizedPath.toLowerCase() : normalizedPath;
150
+ const withinAllowed = allowRoots.some(root => {
151
+ const r = isWindows ? root.toLowerCase() : root;
152
+ return norm === r || norm.startsWith(r + path.sep);
153
+ });
154
+ if (!isAbsolute || !withinAllowed) { res.writeHead(403); res.end('Forbidden'); return; }
128
155
  try {
129
156
  if (!fs.existsSync(normalizedPath)) { res.writeHead(404); res.end('Not found'); return; }
130
157
  const ext = path.extname(normalizedPath).toLowerCase();
@@ -85,8 +85,17 @@ export function register(router, deps) {
85
85
  (async () => {
86
86
  let eventCount = 0;
87
87
  broadcastSync({ type: 'streaming_start', sessionId, agentId, timestamp: Date.now() });
88
+ let claudeSessionBroadcast = false;
88
89
  const onEvent = (parsed) => {
89
90
  eventCount++;
91
+ // Surface claude's REAL session id (from the stream) once, so the client
92
+ // can --resume this conversation on its next turn. The ephemeral
93
+ // 'chat-...' sessionId is not a claude session id and cannot be resumed.
94
+ if (!claudeSessionBroadcast && parsed?.session_id) {
95
+ claudeSessionBroadcast = true;
96
+ ctrl.claudeSessionId = parsed.session_id;
97
+ broadcastSync({ type: 'streaming_session', sessionId, claudeSessionId: parsed.session_id, agentId, timestamp: Date.now() });
98
+ }
90
99
  if (parsed?.type === 'assistant' && parsed.message?.content) {
91
100
  for (const block of parsed.message.content) {
92
101
  broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'assistant', seq: eventCount, timestamp: Date.now() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.948",
3
+ "version": "1.0.949",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -43,7 +43,7 @@
43
43
  "webjsx": "^0.0.73",
44
44
  "webtalk": "^1.0.31",
45
45
  "ws": "^8.14.2",
46
- "xstate": "^5.28.0",
46
+ "xstate": "^5.32.0",
47
47
  "zod": "^4.3.6"
48
48
  },
49
49
  "devDependencies": {
@@ -5,15 +5,16 @@
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
- <!-- 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
- NOTE: re-vendoring 0.0.186+ locally (it adds the load-bearing AgentChat
13
- component) would restore offline + predictability -- tracked in AGENTS.md. -->
14
- <link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
8
+ <!-- The GUI lives in the anentrypoint-design kit and is vendored locally under
9
+ ./vendor/anentrypoint-design so the shipped UI is PREDICTABLE: it does not
10
+ silently shift when the upstream package publishes, and it works offline
11
+ (the markdown stack still fetches marked/dompurify/prismjs on first chat
12
+ render). Update flow: edit the kit at c:\dev\anentrypoint-design, run
13
+ `node scripts/build.mjs`, copy dist/247420.{js,css} here, then publish the
14
+ kit so unpkg stays in sync. -->
15
+ <link rel="stylesheet" href="./vendor/anentrypoint-design/247420.css">
15
16
  <script type="importmap">
16
- { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
17
+ { "imports": { "anentrypoint-design": "./vendor/anentrypoint-design/247420.js" } }
17
18
  </script>
18
19
  <style>
19
20
  :root {
@@ -72,14 +73,8 @@
72
73
  .ds-247420 [data-prog-focus]:focus-visible,
73
74
  [data-prog-focus]:focus, [data-prog-focus]:focus-visible { outline: none !important; box-shadow: none !important; }
74
75
 
75
- /* skip link for keyboard/AT users */
76
- .skip-link {
77
- position: absolute; left: -9999px; top: 0; z-index: 1000;
78
- padding: .5em .9em; border-radius: 6px;
79
- background: var(--accent, var(--agentgui-accent)); color: #06120a;
80
- text-decoration: none; font-weight: 600;
81
- }
82
- .skip-link:focus { left: 8px; top: 8px; outline: 2px solid #fff; outline-offset: 2px; }
76
+ /* (The skip link is rendered + styled by the design system's AppShell;
77
+ agentgui carries no local .skip-link rule.) */
83
78
 
84
79
  /* status connection dot - a CSS-drawn disc (real UI affordance, not a text
85
80
  glyph). The .status-dot-disc child is always present; the parent's state
@@ -260,6 +255,15 @@
260
255
  them a pointer cursor, so the affordance is invisible. */
261
256
  .ds-event-list .row[role="button"] { cursor: pointer; }
262
257
  .ds-event-list .row[role="button"]:hover { background: color-mix(in srgb, var(--fg, var(--agentgui-fg)) 5%, transparent); }
258
+ /* Flash the row a search result landed on, so the match is obvious. */
259
+ .ds-event-list .row.event-flash { animation: agentgui-event-flash 2s ease-out; }
260
+ @keyframes agentgui-event-flash {
261
+ 0% { background: color-mix(in srgb, var(--accent, var(--agentgui-accent)) 40%, transparent); }
262
+ 100% { background: transparent; }
263
+ }
264
+ @media (prefers-reduced-motion: reduce) {
265
+ .ds-event-list .row.event-flash { animation: none; outline: 2px solid var(--accent, var(--agentgui-accent)); }
266
+ }
263
267
 
264
268
  /* Chat composer: hide the idle scrollbar on the (empty/short) textarea. */
265
269
  .chat-composer textarea { overflow-y: auto; scrollbar-width: thin; }