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.
- package/.claude/workflows/gui-audit.js +86 -0
- package/AGENTS.md +15 -1
- package/lib/http-handler.js +29 -2
- package/lib/ws-handlers-util.js +9 -0
- package/package.json +2 -2
- package/site/app/index.html +20 -16
- package/site/app/js/app.js +196 -52
- package/site/app/js/backend.js +5 -1
- package/site/app/vendor/anentrypoint-design/247420.css +923 -46
- package/site/app/vendor/anentrypoint-design/247420.js +89 -42
|
@@ -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
|
|
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.
|
package/lib/http-handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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();
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -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.
|
|
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.
|
|
46
|
+
"xstate": "^5.32.0",
|
|
47
47
|
"zod": "^4.3.6"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
package/site/app/index.html
CHANGED
|
@@ -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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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": "
|
|
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
|
|
76
|
-
|
|
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; }
|