agentgui 1.0.973 → 1.0.975
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 +14 -6
- package/lib/http-handler.js +27 -17
- package/package.json +1 -1
- package/scripts/validate-mutations.mjs +3 -0
- package/site/app/js/app.js +28 -15
- package/site/app/vendor/anentrypoint-design/247420.css +70 -26
- package/site/app/vendor/anentrypoint-design/247420.js +11 -11
- package/site/theme.mjs +2 -2
- package/test.js +0 -89
- package/scripts/harvest-fixtures.mjs +0 -219
- package/site/app/vendor/cdn/dompurify.js +0 -9
- package/site/app/vendor/cdn/fonts/1291de6d401a.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/1ba89a87e0b8.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/3644d51c507b.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/4b91d2650dc2.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/530d036ba64a.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/570a2bdd8f8b.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/5dd6d880fee9.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/62de9143afe3.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/64884efa2f11.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/68cd7063be2e.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/6c252abcf99b.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/71e69e06516a.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/9ea68c62083f.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/c010f9b7d6b2.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/d69723fc74be.woff2 +0 -0
- package/site/app/vendor/cdn/fonts/fonts.css +0 -459
- package/site/app/vendor/cdn/marked.js +0 -8
- package/site/app/vendor/cdn/prismjs/components/prism-bash.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-clike.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-core.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-css.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-diff.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-go.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-javascript.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-json.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-jsx.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-markdown.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-markup.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-python.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-rust.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-sql.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-toml.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-tsx.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-typescript.min.js +0 -1
- package/site/app/vendor/cdn/prismjs/components/prism-yaml.min.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# AgentGUI — Agent Notes
|
|
2
2
|
|
|
3
|
+
## Design-maturity sweep + dead-code + secret-hardening (2026-06-18) — sixteenth run
|
|
4
|
+
|
|
5
|
+
Same mandate ("fix every aspect, all design lives in ../design, fan out subagents, track with a workflow"). One tracking Workflow **`gui-design-16` (run `wf_6479308b-24f`, 12 lenses chat-thread/composer-input/files-browser/sessions-dashboard/shell-chrome/tokens-theme/a11y-motion/history-settings/marketing-site/glyph-residue/dead-code/server-security): 54 agents -> hunt -> adversarial verify -> 40 confirmed findings, ~2.7M tokens.** Applied via a clean per-file subagent fan-out (one agent per physical file, zero edit conflicts).
|
|
6
|
+
|
|
7
|
+
**Kit fixes (ALL in `/config/workspace/design`):** chat.js ToolCallNode now gates the `args` section on real args (`hasArgs`) so result-only/arg-less cards no longer show `args {}` + composer `<textarea>` binds the `disabled` prop; agent-chat.js cwd input emits `aria-busy` while checking; content.js EventList now forwards `detail/actions/highlight/meta` to Row (expanded history events were dropping toolInput detail + copy + search-highlight) + TextField gained a full invalid/error state (`error`/`aria-invalid`/`aria-describedby`/`.ds-field-error` role=alert); interaction-primitives.js ShortcutList splits combos into discrete `.ds-kbd` caps (`.ds-kbd-caps`/`.ds-kbd-sep` + CSS) instead of one wide cap; sessions.js ConversationList rail filter migrated from bespoke `.ds-session-search` to the shared `SearchInput`/`.ds-search-input` (one filter control across rail + dashboard); shell.js WsResizer adds `aria-valuetext "<n> pixels"`; voice.js `×`->`Icon('x')`, community.js `⋯`->`'more'` (glyph residue). chat.css: cwd `.is-checking` hint tone + `aria-busy` field cue + cwd-btn `6px`->`var(--r-1)`, breakdown idle disc moved off the connecting-amber to canonical `--stale` double-inset, breakdown running disc shape-ring, `.ds-dash-clear` hover/focus-visible, `.chat-tool-copy:focus-visible`, errors-toggle AA fix, ds-session-search base block removed. app-shell.css: composer + WorkspaceShell (`.ws-rail/.ws-pane/.ws-sessions/.ws-scrim`) reduced-motion guards, `.ds-file-row.is-locked/.is-restricted` + `.ds-file-perm-tag` chip, `.ws-drawer-toggle` 44px coarse floor, `.ds-density-btn.active`/`.ds-filter-pill.active` AA contrast (`color:var(--fg)`), ShortcutList legend rules. editor-primitives.css `#000` dock shadow->`var(--shadow-3)` + checkbox `#fff` fallback dropped. app-surfaces.css `@media print` re-asserts paper-tuned signal tokens. community.css `:focus-visible` for ThreadPanel/Forum/Page + forum-search. marketing.css NEW `.site-footer` family (theme.mjs footers migrated off the in-app `.app-status` strip that suppressed content <=1100px) + `.site-cli` baseline+card-chrome + token paddings + `100dvh` embed.
|
|
8
|
+
|
|
9
|
+
**Server secret-hardening (`lib/http-handler.js`, the high-sev cluster):** the module-level `SECRET_RE` (already blocking `/api/file`+`/api/download`) is now ALSO applied to `/api/upload-file`, `/api/mkdir`, `/api/rename` (403 on a secret/dotfile target name — was an overwrite-a-secret hole) and `/api/list` (filters secret-named dirents from the listing — was enumerating `.env`/`.pem` the preview/download routes block); `/api/download` Content-Disposition is now RFC-5987 encoded (ASCII `filename=` fallback + `filename*=UTF-8''<pct>`) so a CJK/emoji name no longer throws `ERR_INVALID_CHAR`. `validate-mutations.mjs` 26/26 PASS, no regression.
|
|
10
|
+
|
|
11
|
+
**Dead-code removal:** agentgui `site/app/vendor/cdn/` (36 dead files — marked/dompurify/prismjs/fonts, zero refs; markdown stack fetches jsDelivr at runtime), `scripts/harvest-fixtures.mjs` (zero refs), and the 3 `node scripts/copy-vendor.js` lines in `.github/workflows/build-platforms.yml` (script deleted in the 15th run — CI was broken); kit `scripts/bundle-markdown.mjs` (orphaned, superseded by src/markdown.js).
|
|
12
|
+
|
|
13
|
+
**Witnessed** (localhost:3009/gm/?token, PASSWORD=`123,slam,123,slam`, fresh server + re-vendored dist, via gm `browser` verb): readyState complete, `ds-247420` dark body `rgb(19,19,24)`, 3 resizers, hScroll 0, rail `ds-search-input`=1 / bespoke `ds-session-search`=0 (consolidation landed), `ds-file-perm-tag`/`site-footer`/`ds-kbd-caps` rules all resolve, **0 console errors**. Kit build all 4 lints PASS. `test.js` 10 pass/0 fail. Re-vendored `dist/247420.{css,js}`.
|
|
14
|
+
|
|
3
15
|
## Design-maturity sweep + dead-code + server-hardening (2026-06-18) — fifteenth run
|
|
4
16
|
|
|
5
17
|
Mandate: "fix every aspect, update ../design, all design content lives there, fan out subagents, track with a workflow." Two tracking Workflows ran. **`gui-design-15` (run `wf_7ba315a1-9a9`, 8 lenses composer-input/chat-thread/files/sessions/shell/tokens-theme/a11y-motion/glyph-residue): 59 agents -> 42 findings -> 35 confirmed -> 33 applied across 9 kit files** + 2 deferred fixes implemented after (shell pane-toggle relocated into `.ws-crumb` as a `ws-desktop-toggle`; files density picker made icon-led with 3 NEW shell.js icons `rows`/`rows-tight`/`grid` + `DENSITY_ICONS` + `.ds-density-btn` 28px square). **`gui-design-15b` (run `wf_5b2861b2-717`, 3 lenses history-settings/server-boundary/security): 25 agents -> 22 findings -> 18 confirmed**, all landed. Total ~95 agents, ~4M tokens.
|
|
@@ -14,15 +26,11 @@ Mandate: "fix every aspect, update ../design, all design content lives there, fa
|
|
|
14
26
|
|
|
15
27
|
## Design-maturity sweep + marketing-site consolidation (2026-06-18) — fourteenth run
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
**Workflow `gui-design-14` (via the Workflow tool, run `wf_50751bd9-a43`, 7 lenses: chat-thread/files/sessions/shell-chrome/tokens-theme/a11y-motion/marketing-residue): 46 agents -> 39 findings -> 28 adversarially confirmed** (`PUNCHLIST-DESIGN-14.md`; 2 lens connection-failures: tokens-theme hunt + marketing-residue verify). ALL landed in the kit. **chat:** ThinkingNode base CSS (`.chat-thinking`/`-text`/`-dots` had ZERO base rules outside `.agentchat-working`), markdown `table`/`th`/`td`/`hr` (were unstyled -> bare cells), dead `.chat-tick` inline-code class given a rule, composer toolbar unified on 32px/`--r-1` (was 40px-round vs 36px-square), flat `.chat-md` rhythm+inset, streaming `.chat-stream-pre` persistent chrome, tool/code `:has()` measure-breakout. **files:** FileSkeleton container was a single 48px shimmer bar collapsing all rows (now inner `.ds-skel`-only), filter folded into one `.ds-file-controls` toolbar row, per-type cell icon tints, dead `data-columns` card-mode removed (+ `FILE_GRID_MIN_COL`/`columns` param), `.ds-file-check` dead font/color decls dropped + `.ds-check-box` rest border -> `--fg-3`. **sessions:** stale disc own tone (was sharing `--amber` with connecting) + connecting now a hollow ring, dead duplicate `.ds-dash-status.is-running` removed + running unified on `--accent` across disc/breakdown/card, `.seg.is-idle` weight 600, reduced-motion live-disc static-ring fallback, mixed-tick thickened 1.5px->2px, card cost as emphasized `.ds-dash-stat-cost`, conv-list unread -> hollow ring (shape-distinct from running). **shell:** resizers hidden past their staging breakpoints (`@media` 1480/1100/900 — were dead handles dragging vars into a fixed drawer), coarse-pointer 16px hit target, `WS_RESIZE_CLAMP` reconciled to the CSS `clamp()` floors/ceilings, localStorage committed on pointerup (not per-move), separator `aria-valuemin/max/now`. **a11y:** voice `vx-*` added to `community.css` reduced-motion guard, status discs given non-colour SHAPE channels, `.ds-input-bare` `:focus`->`:focus-visible`+ring. **Status-disc shape rule:** live=solid+pulse(or static ring under reduced-motion), error=solid+halo, connecting=hollow ring, stale=muted solid — four channels, not colour-only. Witnessed live (localhost:3009/gm/?token, PASSWORD=`123,slam,123,slam`): 0 console/page errors, dark body surface painted by kit, 3 resizers, no h-scroll, migrated classes resolve. Kit pushed `3704876`; rebased onto a concurrent CI `v0.0.211` bump.
|
|
29
|
+
`site/theme.mjs` (marketing renderer, NOT the SPA) was the last design residue -> migrated to a NEW kit `marketing.css` `.site-*` family; `Panel`/`Heading` gained a `class` prop; `Btn({primary})`->`Btn({variant:'primary'})`. Workflow `gui-design-14` (`wf_50751bd9-a43`, 7 lenses) 46 agents -> 28 confirmed, all in the kit (ThinkingNode/markdown-table/status-disc shape channels/shell resizer breakpoints). **Status-disc shape rule: live=solid+pulse, error=solid+halo, connecting=hollow ring, stale=muted solid.** Full detail in rs-learn (recall "agentgui 14th run marketing-site consolidation").
|
|
20
30
|
|
|
21
31
|
## Design-content consolidation — ALL design lives in the kit now (2026-06-18) — thirteenth run
|
|
22
32
|
|
|
23
|
-
The mandate: every design decision
|
|
24
|
-
|
|
25
|
-
**Workflow `.claude/workflows/gui-design-consolidation.js`** (residue-first 13th run, 6 lenses: design-residue/chat-thread/files-live/shell-chrome/tokens-theme/a11y-motion) ran 44 agents -> 19 confirmed findings (`PUNCHLIST-DESIGN.md`), ALL landed in the kit: composer send-row `.chat-composer-toolbar` (was unstyled), role-monotonic chat hover tints, tool-card `done` success tone, base-`Chat` `.chat-empty-suggestion` styling, error-card flame inset rail, `.ds-check-box` CSS-drawn multi-select checkbox (replaced the `[x]/[ ]` bracket text in files.js+sessions.js with a bordered box that fills on `.is-marked`/`[aria-checked]` — border-drawn tick, NOT a glyph), thin 26px status strip, status-item ellipsis, `.ws-crumb` gutter align, fluid `clamp()` shell width tokens, `:focus`->`:focus-visible` (no ring on mouse click), `--warn`/`--sky` dark-theme pairs, unified `--focus-*` tokens, reduced-motion skeleton static-fill, color-blind-safe rail SHAPE differentiation + sr-only status word in `Row()`. Amber/purple hardcoded `var(--token, #hex)` fallbacks stripped (the dark-hex could wrongly paint the light theme). **`lint-glyphs` was extended** with `SCAN_ROOT_FILES` (app-shell/chat/colors_and_type/community/community-app/editor-primitives/app-surfaces.css) — the root bundled CSS previously escaped the glyph lint, which is how 39 box-drawing comment dividers in app-shell.css slipped in (now converted to ASCII). Witnessed live (localhost:3009/gm/, fresh bundle): 0 console/page errors, migrated classes resolve from the kit bundle (`.pill` cursor:pointer/radius 999px, `.ds-check-box` 15px, cwd mono), no h-scroll.
|
|
33
|
+
The mandate's origin: every design decision lives in the kit (`../design`), none in agentgui. `site/app/index.html` lost its ~240-line inline `<style>` (moved to NEW kit `app-surfaces.css`); `app.js` carries zero inline `style=` props. **Rule going forward: no design content in agentgui — new surface styling is a kit CSS rule, never an inline `<style>` or `style=`.** `lint-glyphs` extended with `SCAN_ROOT_FILES` so root bundled CSS no longer escapes the glyph lint. Workflow `gui-design-consolidation.js` (6 lenses) 44 agents -> 19 confirmed. Full detail in rs-learn (recall "agentgui 13th run design-content consolidation").
|
|
26
34
|
|
|
27
35
|
## CRITICAL — `authedFetch` must NOT set `Authorization: Bearer` behind an nginx Basic-Auth proxy (2026-06-18)
|
|
28
36
|
|
package/lib/http-handler.js
CHANGED
|
@@ -37,6 +37,12 @@ export function confineToRoots(inputPath, allowRoots) {
|
|
|
37
37
|
return { ok: true, realPath };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Secret-bearing basenames that must never be readable through ANY confined
|
|
41
|
+
// raw-bytes route (preview or download), even when they sit inside an allowed
|
|
42
|
+
// root: dotfiles, env/key/cert material, and credential stores. One const so
|
|
43
|
+
// /api/file and /api/download can never drift apart.
|
|
44
|
+
export const SECRET_RE = /(^\.|\.(env|pem|key|crt|p12|pfx)$|secret|credential|\.npmrc$|\.netrc$)/i;
|
|
45
|
+
|
|
40
46
|
// The allowlist the Files surface operates within: server cwd + Claude
|
|
41
47
|
// projects dir, widened via FS_ROOTS (path-separated). One construction so
|
|
42
48
|
// /api/list,file,download and the mutation routes can never drift apart.
|
|
@@ -95,19 +101,15 @@ function isAllowRoot(realPath, allowRoots) {
|
|
|
95
101
|
|
|
96
102
|
export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, PORT }) {
|
|
97
103
|
return async function httpHandler(req, res) {
|
|
98
|
-
// CORS: when
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// when configured, else same-origin only. With no PASSWORD the server is
|
|
103
|
-
// already open, so the permissive wildcard is harmless and kept for tools.
|
|
104
|
+
// CORS: emit ACAO only when CORS_ORIGIN is explicitly set. A wildcard would
|
|
105
|
+
// let any webpage the user visits make credentialless fetches to /api/list,
|
|
106
|
+
// /api/file/*, etc. and read ~/.claude/projects content — even on a no-
|
|
107
|
+
// PASSWORD localhost deploy. Set CORS_ORIGIN=<origin> for cross-origin tools.
|
|
104
108
|
const _corsOrigin = process.env.CORS_ORIGIN;
|
|
105
109
|
if (_corsOrigin) {
|
|
106
110
|
res.setHeader('Access-Control-Allow-Origin', _corsOrigin);
|
|
107
111
|
res.setHeader('Vary', 'Origin');
|
|
108
|
-
}
|
|
109
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
110
|
-
} // else: no ACAO header -> browsers block cross-origin reads (same-origin still works)
|
|
112
|
+
} // no ACAO header -> browsers enforce same-origin by default
|
|
111
113
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
112
114
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
113
115
|
// The password can ride in a ?token= query param (EventSource/deep-links
|
|
@@ -403,7 +405,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
403
405
|
for (const [t, exts] of Object.entries(EXT_TYPE)) if (exts.includes(ext)) return t;
|
|
404
406
|
return 'other';
|
|
405
407
|
};
|
|
406
|
-
const dirents = fs.readdirSync(normalizedPath, { withFileTypes: true });
|
|
408
|
+
const dirents = fs.readdirSync(normalizedPath, { withFileTypes: true }).filter((d) => !SECRET_RE.test(d.name));
|
|
407
409
|
const entries = dirents.map((d) => {
|
|
408
410
|
const full = path.join(normalizedPath, d.name);
|
|
409
411
|
let size = null, modified = null, permissions;
|
|
@@ -449,7 +451,6 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
449
451
|
// material, and credential stores must never be readable through the
|
|
450
452
|
// Files preview even when they sit inside an allowed root.
|
|
451
453
|
const base = path.basename(normalizedPath);
|
|
452
|
-
const SECRET_RE = /(^\.|\.(env|pem|key|crt|p12|pfx)$|secret|credential|\.npmrc$|\.netrc$)/i;
|
|
453
454
|
if (SECRET_RE.test(base)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
454
455
|
// Only known text/code extensions (images go through /api/image). An
|
|
455
456
|
// unknown/binary extension is rejected, never served as octet-stream.
|
|
@@ -493,15 +494,19 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
493
494
|
const conf = confineToRoots(decodedPath, allowRoots);
|
|
494
495
|
if (!conf.ok) { res.writeHead(conf.reason === 'not found' ? 404 : 403); res.end('Forbidden'); return; }
|
|
495
496
|
const normalizedPath = conf.realPath;
|
|
497
|
+
// Same secret-name block as /api/file: a download must not exfiltrate
|
|
498
|
+
// .env/.pem/.key/credential material just because it streams bytes.
|
|
499
|
+
if (SECRET_RE.test(path.basename(normalizedPath))) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
496
500
|
try {
|
|
497
501
|
const st = fs.statSync(normalizedPath);
|
|
498
502
|
if (!st.isFile()) { res.writeHead(400); res.end('Not a file'); return; }
|
|
499
503
|
const MAX = 50 * 1024 * 1024; // 50MB cap so a download can't exhaust memory
|
|
500
504
|
if (st.size > MAX) { res.writeHead(413); res.end('File too large to download'); return; }
|
|
501
|
-
const
|
|
505
|
+
const base = path.basename(normalizedPath);
|
|
506
|
+
const asciiName = base.replace(/[\\"\r\n]/g, '').replace(/[^\x20-\x7e]/g, '_');
|
|
502
507
|
res.writeHead(200, {
|
|
503
508
|
'Content-Type': 'application/octet-stream',
|
|
504
|
-
'Content-Disposition': 'attachment; filename="' +
|
|
509
|
+
'Content-Disposition': 'attachment; filename="' + asciiName + '"; filename*=UTF-8\'\'' + encodeURIComponent(base),
|
|
505
510
|
'Content-Length': String(st.size),
|
|
506
511
|
'Cache-Control': 'no-cache',
|
|
507
512
|
});
|
|
@@ -533,6 +538,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
533
538
|
if (isAllowRoot(conf.realPath, allowRoots)) { sendJSON(req, res, 403, { error: 'forbidden: cannot rename an allowed root' }); return; }
|
|
534
539
|
const newName = sanitizeEntryName(body.newName);
|
|
535
540
|
if (!newName) { sendJSON(req, res, 400, { error: 'invalid name' }); return; }
|
|
541
|
+
if (SECRET_RE.test(newName)) { sendJSON(req, res, 403, { error: 'forbidden: secret/dotfile name' }); return; }
|
|
536
542
|
const target = path.join(path.dirname(conf.realPath), newName);
|
|
537
543
|
// The target stays in the same (already-confined) directory by
|
|
538
544
|
// construction, but re-check anyway so the invariant is local.
|
|
@@ -622,6 +628,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
622
628
|
if (!conf.ok) { sendJSON(req, res, conf.reason === 'not found' ? 404 : 403, { error: 'forbidden: ' + conf.reason }); return; }
|
|
623
629
|
const name = sanitizeEntryName(body.name);
|
|
624
630
|
if (!name) { sendJSON(req, res, 400, { error: 'invalid name' }); return; }
|
|
631
|
+
if (SECRET_RE.test(name)) { sendJSON(req, res, 403, { error: 'forbidden: secret/dotfile name' }); return; }
|
|
625
632
|
const target = path.join(conf.realPath, name);
|
|
626
633
|
if (fs.existsSync(target)) { sendJSON(req, res, 409, { error: 'a file with that name already exists' }); return; }
|
|
627
634
|
try { fs.mkdirSync(target); sendJSON(req, res, 200, { ok: true, path: target }); }
|
|
@@ -641,6 +648,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
641
648
|
if (!conf.ok) { sendJSON(req, res, conf.reason === 'not found' ? 404 : 403, { error: 'forbidden: ' + conf.reason }); return; }
|
|
642
649
|
const name = sanitizeEntryName(qs.get('name'));
|
|
643
650
|
if (!name) { sendJSON(req, res, 400, { error: 'invalid name' }); return; }
|
|
651
|
+
if (SECRET_RE.test(name)) { sendJSON(req, res, 403, { error: 'forbidden: secret/dotfile name' }); return; }
|
|
644
652
|
const target = path.join(conf.realPath, name);
|
|
645
653
|
if (fs.existsSync(target) && qs.get('overwrite') !== '1') { sendJSON(req, res, 409, { error: 'a file with that name already exists' }); return; }
|
|
646
654
|
let buf;
|
|
@@ -672,12 +680,14 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
672
680
|
const normalizedPath = conf.realPath;
|
|
673
681
|
try {
|
|
674
682
|
const ext = path.extname(normalizedPath).toLowerCase();
|
|
675
|
-
const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp'
|
|
676
|
-
//
|
|
677
|
-
//
|
|
683
|
+
const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' };
|
|
684
|
+
// SVG is intentionally excluded: browsers render SVG as a live document
|
|
685
|
+
// in the app's origin, so an agent-written SVG with a <script src=CDN>
|
|
686
|
+
// would execute in the agentgui origin (CSP allows unpkg/jsdelivr).
|
|
687
|
+
// Files preview uses /api/file/download (attachment) for SVG.
|
|
678
688
|
const contentType = mimeTypes[ext];
|
|
679
689
|
if (!contentType) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
680
|
-
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
690
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache', 'X-Content-Type-Options': 'nosniff' });
|
|
681
691
|
res.end(fs.readFileSync(normalizedPath));
|
|
682
692
|
} catch (err) { sendJSON(req, res, 400, { error: err.message }); }
|
|
683
693
|
return;
|
package/package.json
CHANGED
|
@@ -28,6 +28,9 @@ const TRAVERSAL = join(ROOT, '..', '..', ...(WIN ? ['Windows', 'win.ini'] : ['et
|
|
|
28
28
|
await check('rename-traversal', 403, await post('/api/rename', { path: TRAVERSAL, newName: 'x.txt' }));
|
|
29
29
|
await check('delete-out-of-roots', 403, await post('/api/delete', { path: OUTSIDE }));
|
|
30
30
|
await check('delete-root-refused', 403, await post('/api/delete', { path: ROOT }));
|
|
31
|
+
await check('rename-to-secret-403', 403, await post('/api/rename', { path: join(ROOT, 'package.json'), newName: '.env' }));
|
|
32
|
+
await check('upload-secret-403', 403, await fetch(BASE + '/api/upload-file?dir=' + encodeURIComponent(ROOT) + '&name=.env', { method: 'PUT', headers: AUTH, body: 'secret' }).then(async r => ({ status: r.status, body: await r.text() })));
|
|
33
|
+
await check('mkdir-secret-403', 403, await post('/api/mkdir', { dir: ROOT, name: '.env' }));
|
|
31
34
|
await check('mkdir-reserved-name', 400, await post('/api/mkdir', { dir: ROOT, name: 'CON' }));
|
|
32
35
|
await check('mkdir-name-with-separator', 400, await post('/api/mkdir', { dir: ROOT, name: 'a/b' }));
|
|
33
36
|
await check('mkdir-name-trailing-dot', 400, await post('/api/mkdir', { dir: ROOT, name: 'evil.' }));
|
package/site/app/js/app.js
CHANGED
|
@@ -415,6 +415,7 @@ function openLiveStream() {
|
|
|
415
415
|
// Dedupe against the snapshot/prior pushes by event index - a
|
|
416
416
|
// reconnect or overlap would otherwise double-append the same event.
|
|
417
417
|
if (ev.i == null || !state.events.some(e => e.i === ev.i)) {
|
|
418
|
+
ev._idx = state.events.length;
|
|
418
419
|
state.events.push(ev);
|
|
419
420
|
// Cap retained events so a long live session can't grow unbounded.
|
|
420
421
|
if (state.events.length > 2000) state.events.splice(0, state.events.length - 2000);
|
|
@@ -1699,7 +1700,9 @@ function appendText(parts, text) {
|
|
|
1699
1700
|
// standalone tool_result part. Sets the card's result + done/error status.
|
|
1700
1701
|
function applyToolResult(parts, block) {
|
|
1701
1702
|
const id = block.tool_use_id || block.id || null;
|
|
1702
|
-
const
|
|
1703
|
+
const raw = block?.content ?? block?.output ?? block;
|
|
1704
|
+
// Claude API delivers content as an array of {type,text} objects; flatten to plain text.
|
|
1705
|
+
const content = Array.isArray(raw) ? (raw.filter(b => b.type === 'text').map(b => b.text).join('\n') || JSON.stringify(raw, null, 2)) : raw;
|
|
1703
1706
|
const isError = !!(block?.is_error);
|
|
1704
1707
|
const byId = id ? [...parts].reverse().find(p => p && p.kind === 'tool' && p._id === id) : null;
|
|
1705
1708
|
const target = byId || [...parts].reverse().find(p => p && p.kind === 'tool' && p.status === 'running');
|
|
@@ -2323,6 +2326,9 @@ async function sendChat(textArg) {
|
|
|
2323
2326
|
} finally {
|
|
2324
2327
|
state.chat.busy = false;
|
|
2325
2328
|
state.chat.abort = null;
|
|
2329
|
+
// Prune an empty assistant shell (WS-drop before any content arrived).
|
|
2330
|
+
const msgs = state.chat.messages;
|
|
2331
|
+
if (msgs.length && isEmptyTurn(msgs[msgs.length - 1])) msgs.pop();
|
|
2326
2332
|
persistChat();
|
|
2327
2333
|
refreshActive(); // settle the running panel/dashboard now, not at the next poll
|
|
2328
2334
|
render();
|
|
@@ -2479,6 +2485,13 @@ function historyMain() {
|
|
|
2479
2485
|
onSelect: (id) => { state.eventFilter = id && id.id ? id.id : id; render(); },
|
|
2480
2486
|
label: 'Filter events by type',
|
|
2481
2487
|
});
|
|
2488
|
+
// Single pass over state.events for all three counters (replaces three separate .filter() calls).
|
|
2489
|
+
const evCounters = state.events.reduce((c, e) => {
|
|
2490
|
+
if (e.role === 'user') c.turns++;
|
|
2491
|
+
if (e.type === 'tool_use') c.tools++;
|
|
2492
|
+
if (e.isError) c.errors++;
|
|
2493
|
+
return c;
|
|
2494
|
+
}, { turns: 0, tools: 0, errors: 0 });
|
|
2482
2495
|
const meta = SessionMeta({
|
|
2483
2496
|
items: [
|
|
2484
2497
|
sess && sess.cwd ? { label: 'cwd', value: sess.cwd, title: sess.cwd } : null,
|
|
@@ -2487,9 +2500,9 @@ function historyMain() {
|
|
|
2487
2500
|
// Spelled counter vocabulary in the detail strip (events/turns/tools/
|
|
2488
2501
|
// errors); the abbreviated 'ev/tools/err' triple stays compact-row-only.
|
|
2489
2502
|
{ label: 'events', value: String(state.events.length) },
|
|
2490
|
-
{ label: 'turns', value: String(sess?.userTurns ??
|
|
2491
|
-
{ label: 'tools', value: String(
|
|
2492
|
-
{ label: 'errors', value: String(
|
|
2503
|
+
{ label: 'turns', value: String(sess?.userTurns ?? evCounters.turns) },
|
|
2504
|
+
{ label: 'tools', value: String(evCounters.tools) },
|
|
2505
|
+
{ label: 'errors', value: String(evCounters.errors) },
|
|
2493
2506
|
].filter(Boolean),
|
|
2494
2507
|
});
|
|
2495
2508
|
if (filteredEvents.length === 0) {
|
|
@@ -2506,10 +2519,7 @@ function historyMain() {
|
|
|
2506
2519
|
const shown = filteredEvents.slice(-limit);
|
|
2507
2520
|
const hiddenCount = total - shown.length;
|
|
2508
2521
|
// Keys of the currently-shown rows, so expand-all toggles only what's rendered.
|
|
2509
|
-
const shownKeys = shown.map((e, i) =>
|
|
2510
|
-
const absIdx = total - shown.length + i;
|
|
2511
|
-
return e.i != null ? 'ev' + e.i : 'ev-' + (e.ts || 0) + '-' + (e.type || '') + '-' + absIdx;
|
|
2512
|
-
});
|
|
2522
|
+
const shownKeys = shown.map((e, i) => e.i != null ? 'ev' + e.i : 'ev-' + (e.ts || 0) + '-' + (e.type || '') + '-' + (e._idx ?? (total - shown.length + i)));
|
|
2513
2523
|
const allExpanded = shownKeys.length > 0 && shownKeys.every(k => state.expandedEvents.has(k));
|
|
2514
2524
|
const eventControls = h('div', { key: 'evctrl', class: 'history-actions', role: 'group', 'aria-label': 'event controls' },
|
|
2515
2525
|
Btn({ key: 'expall', onClick: () => {
|
|
@@ -2535,8 +2545,7 @@ function historyMain() {
|
|
|
2535
2545
|
// Stable key: server event index when present, else ts + the event's
|
|
2536
2546
|
// ABSOLUTE position in state.events (not the sliced-view index, which
|
|
2537
2547
|
// shifts when live events append and would collide loaded vs live rows).
|
|
2538
|
-
const
|
|
2539
|
-
const key = e.i != null ? 'ev' + e.i : 'ev-' + (e.ts || 0) + '-' + (e.type || '') + '-' + absIdx;
|
|
2548
|
+
const key = e.i != null ? 'ev' + e.i : 'ev-' + (e.ts || 0) + '-' + (e.type || '') + '-' + (e._idx ?? (total - shown.length + i));
|
|
2540
2549
|
const role = e.role || '?';
|
|
2541
2550
|
const type = e.type || '?';
|
|
2542
2551
|
const tool = e.tool ? ' · tool: ' + e.tool : '';
|
|
@@ -2890,8 +2899,7 @@ function settingsMain() {
|
|
|
2890
2899
|
label: 'backend url',
|
|
2891
2900
|
value: state.backendDraft,
|
|
2892
2901
|
placeholder: '(blank = same origin)',
|
|
2893
|
-
|
|
2894
|
-
'aria-invalid': !isValid ? 'true' : 'false',
|
|
2902
|
+
error: !isValid ? 'Invalid URL format' : undefined,
|
|
2895
2903
|
title: isValid ? 'Enter a valid URL or leave blank for same-origin' : 'Invalid URL format',
|
|
2896
2904
|
onInput: (v) => {
|
|
2897
2905
|
state.backendDraft = v;
|
|
@@ -2900,7 +2908,6 @@ function settingsMain() {
|
|
|
2900
2908
|
render();
|
|
2901
2909
|
},
|
|
2902
2910
|
}),
|
|
2903
|
-
!isValid ? h('p', { key: 'err', id: 'backend-url-error', class: 'lede field-error', role: 'alert' }, 'Invalid URL format') : null,
|
|
2904
2911
|
state.backendStatus === 'connecting' ? h('p', { key: 'bst-connecting', class: 'lede', role: 'status' }, 'connecting…') : null,
|
|
2905
2912
|
state.backendStatus === 'ok' ? h('p', { key: 'bst-ok', class: 'lede', role: 'status' }, 'connected') : null,
|
|
2906
2913
|
state.backendStatus === 'failed' ? h('p', { key: 'bst-failed', class: 'lede field-error', role: 'alert' }, 'connection failed - check the URL') : null,
|
|
@@ -2977,6 +2984,8 @@ function clearLocalData() {
|
|
|
2977
2984
|
// defaults with the keys gone.
|
|
2978
2985
|
state.chat.abort?.abort(); // stop any in-flight stream before we drop the page
|
|
2979
2986
|
for (const k of ['agentgui.chat', 'agentgui.agent', 'agentgui.model', 'agentgui.cwd', 'agentgui.backend', 'agentgui.live', 'agentgui.files']) lsRemove(k);
|
|
2987
|
+
// Also wipe kit WorkspaceShell layout keys (collapse state + resizer widths).
|
|
2988
|
+
try { for (let i = localStorage.length - 1; i >= 0; i--) { const k = localStorage.key(i); if (k && k.startsWith('ds.ws.')) localStorage.removeItem(k); } } catch {}
|
|
2980
2989
|
state.chat = { messages: [], busy: false, abort: null, draft: '', resumeSid: null, confirmingEdit: null, totalCost: 0 };
|
|
2981
2990
|
location.reload();
|
|
2982
2991
|
}
|
|
@@ -3013,7 +3022,7 @@ function preferencesPanel() {
|
|
|
3013
3022
|
state.confirmingClearData
|
|
3014
3023
|
? Alert({ key: 'cld', kind: 'warn', title: 'Clear all local data?',
|
|
3015
3024
|
children: [
|
|
3016
|
-
h('span', { key: 'cldtxt' }, 'Removes saved chat, agent/model/cwd, and
|
|
3025
|
+
h('span', { key: 'cldtxt' }, 'Removes saved chat, agent/model/cwd, backend, and layout preferences from this browser. This cannot be undone. '),
|
|
3017
3026
|
Btn({ key: 'cldno', onClick: () => { state.confirmingClearData = false; render(); }, children: 'cancel' }),
|
|
3018
3027
|
Btn({ key: 'cldyes', danger: true, onClick: clearLocalData, children: 'clear' })] })
|
|
3019
3028
|
: Btn({ key: 'cldbtn', onClick: clearLocalData, children: 'clear local data' }),
|
|
@@ -3046,7 +3055,9 @@ function agentsPanel() {
|
|
|
3046
3055
|
const bits = [PROTOCOL_WORDS[a.protocol] || 'agent'];
|
|
3047
3056
|
if (!avail) bits.push(a.npxInstallable ? 'runs via npx' : 'not installed');
|
|
3048
3057
|
if (acp) bits.push(acp.healthy ? 'running healthy' : (acp.running ? 'running' : 'stopped'));
|
|
3049
|
-
if (acp && acp.restartCount
|
|
3058
|
+
if (acp && acp.restartCount >= 1) bits.push('restarted ' + acp.restartCount + (acp.restartCount === 1 ? ' time' : ' times'));
|
|
3059
|
+
if (acp && !acp.healthy && acp.providerInfo?.error) bits.push(acp.providerInfo.error);
|
|
3060
|
+
if (acp && acp.idleMs > 3_600_000) bits.push(fmtDuration(acp.idleMs) + ' idle');
|
|
3050
3061
|
return Row({
|
|
3051
3062
|
key: 'ag' + a.id,
|
|
3052
3063
|
rank: String(i + 1).padStart(3, '0'),
|
|
@@ -3192,6 +3203,8 @@ async function loadSession(sid, { focusEventI = null, focusEventTs = null, fromH
|
|
|
3192
3203
|
// whole session) - cap in-memory state at the most-recent 5000 so a
|
|
3193
3204
|
// monster session can't pin the tab; the render window stays 300+load-older.
|
|
3194
3205
|
if (state.events.length > 5000) state.events = state.events.slice(-5000);
|
|
3206
|
+
// Stamp stable _idx so EventList keys are stable regardless of slice/cap.
|
|
3207
|
+
state.events.forEach((e, i) => { if (e._idx == null) e._idx = i; });
|
|
3195
3208
|
clearTimeout(slowTimer);
|
|
3196
3209
|
state.eventsSlow = false;
|
|
3197
3210
|
state.eventsLoaded = true;
|
|
@@ -1809,7 +1809,7 @@
|
|
|
1809
1809
|
transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
|
|
1810
1810
|
}
|
|
1811
1811
|
.ds-247420 .ds-density-btn:hover { color: var(--fg); }
|
|
1812
|
-
.ds-247420 .ds-density-btn.active { background: var(--accent-tint); color: var(--
|
|
1812
|
+
.ds-247420 .ds-density-btn.active { background: var(--accent-tint); color: var(--fg); }
|
|
1813
1813
|
.ds-247420 .ds-density-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1814
1814
|
|
|
1815
1815
|
/* Compact density — tighter rows for long listings. */
|
|
@@ -1834,6 +1834,10 @@
|
|
|
1834
1834
|
.ds-247420 .ds-file-cell.active { border-color: var(--accent); background: var(--accent-tint); }
|
|
1835
1835
|
.ds-247420 .ds-file-cell.is-marked { border-color: var(--accent); background: var(--accent-tint); }
|
|
1836
1836
|
.ds-247420 .ds-file-cell.is-locked { opacity: 0.6; }
|
|
1837
|
+
.ds-247420 .ds-file-row.is-locked { opacity: 0.6; }
|
|
1838
|
+
.ds-247420 .ds-file-row.is-restricted .title { color: var(--fg-2); }
|
|
1839
|
+
.ds-247420 .ds-file-perm-tag { font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-3); padding: 1px 6px; border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1); white-space: nowrap; }
|
|
1840
|
+
.ds-247420 .ds-file-perm-tag.is-noaccess { color: var(--flame); border-color: color-mix(in srgb, var(--flame) 40%, transparent); }
|
|
1837
1841
|
.ds-247420 .ds-file-cell-open {
|
|
1838
1842
|
display: flex; flex-direction: column; align-items: stretch; gap: var(--space-1);
|
|
1839
1843
|
width: 100%; padding: var(--space-1); margin: 0;
|
|
@@ -2230,6 +2234,10 @@
|
|
|
2230
2234
|
border-radius: var(--r-2); font-size: var(--fs-xs);
|
|
2231
2235
|
}
|
|
2232
2236
|
.ds-247420 .ds-shortcut-row { display: flex; align-items: center; gap: var(--space-2); }
|
|
2237
|
+
.ds-247420 .ds-shortcuts-hint .ds-kbd-label { flex: 1 1 auto; opacity: .85; }
|
|
2238
|
+
.ds-247420 .ds-shortcuts-hint .ds-kbd { white-space: normal; max-width: 100%; }
|
|
2239
|
+
.ds-247420 .ds-kbd-caps { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
2240
|
+
.ds-247420 .ds-kbd-sep { color: var(--fg-3); font-size: var(--fs-micro); padding: 0 2px; }
|
|
2233
2241
|
.ds-247420 .ds-kbd {
|
|
2234
2242
|
display: inline-block; min-width: 0;
|
|
2235
2243
|
padding: 2px 7px; border-radius: 6px;
|
|
@@ -2667,9 +2675,10 @@
|
|
|
2667
2675
|
re-declare it; the sizing block now states the truth). */
|
|
2668
2676
|
line-height: 1.5; resize: none;
|
|
2669
2677
|
min-height: 28px; max-height: 200px;
|
|
2670
|
-
box-sizing: border-box; overflow-y:
|
|
2678
|
+
box-sizing: border-box; overflow-y: hidden;
|
|
2671
2679
|
scrollbar-width: thin;
|
|
2672
2680
|
}
|
|
2681
|
+
.ds-247420 .chat-composer textarea:focus { overflow-y: auto; }
|
|
2673
2682
|
.ds-247420 .chat-composer textarea::placeholder { color: var(--fg-3); }
|
|
2674
2683
|
.ds-247420 .chat-composer textarea:focus { background: none; border: none; box-shadow: none; outline: none; }
|
|
2675
2684
|
.ds-247420 .chat-composer .send {
|
|
@@ -2702,6 +2711,12 @@
|
|
|
2702
2711
|
.ds-247420 .chat-composer .send:disabled {
|
|
2703
2712
|
background: var(--bg-3); color: var(--fg-3); cursor: not-allowed; transform: none;
|
|
2704
2713
|
}
|
|
2714
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2715
|
+
.ds-247420 .chat-composer,
|
|
2716
|
+
.ds-247420 .chat-composer textarea,
|
|
2717
|
+
.ds-247420 .chat-composer .send,
|
|
2718
|
+
.ds-247420 .composer-btn { transition: none; }
|
|
2719
|
+
}
|
|
2705
2720
|
|
|
2706
2721
|
.ds-247420 .aicat-portrait { display: inline-flex; align-items: center; gap: 10px; padding: 4px 0; }
|
|
2707
2722
|
.ds-247420 .aicat-face {
|
|
@@ -3694,6 +3709,8 @@
|
|
|
3694
3709
|
.ds-247420 .ws-pane-collapsed .ws-pane > * { display: none; }
|
|
3695
3710
|
@media (pointer: coarse) {
|
|
3696
3711
|
.ds-247420 .ws-rail-toggle { width: 44px; height: 44px; }
|
|
3712
|
+
.ds-247420 .ws-drawer-toggle { width: 44px; height: 44px; }
|
|
3713
|
+
.ds-247420 .ws-desktop-toggle { width: 44px; height: 44px; }
|
|
3697
3714
|
}
|
|
3698
3715
|
|
|
3699
3716
|
/* Drawer toggles and the scrim are hidden by default and revealed by the staged
|
|
@@ -3711,6 +3728,7 @@
|
|
|
3711
3728
|
pane becomes a mobile overlay drawer at <=1480px, reached via its own
|
|
3712
3729
|
drawer-toggle), so hide this crumb control past that breakpoint. */
|
|
3713
3730
|
@media (max-width: 1480px) { .ds-247420 .ws-pane-toggle { display: none; } }
|
|
3731
|
+
@media (max-width: 480px) { .ds-247420 .ws-crumb { padding-left: var(--space-2); padding-right: var(--space-2); } }
|
|
3714
3732
|
.ds-247420 .ws-scrim { display: none; }
|
|
3715
3733
|
|
|
3716
3734
|
/* Responsive: the columns yield to the CONTENT in stages - the main column is
|
|
@@ -3819,6 +3837,11 @@
|
|
|
3819
3837
|
.ds-247420 .ws-drawer-toggle:hover { background: var(--bg-2); color: var(--fg); }
|
|
3820
3838
|
.ds-247420 .ws-drawer-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
3821
3839
|
|
|
3840
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3841
|
+
.ds-247420 .ws-shell, .ds-247420 .ws-rail, .ds-247420 .ws-pane, .ds-247420 .ws-sessions, .ds-247420 .ws-scrim { transition: none; }
|
|
3842
|
+
.ds-247420 .ws-resizer::after { transition: none; }
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3822
3845
|
/* ============================================================
|
|
3823
3846
|
Row title highlight, expanded-row actions, filter pills.
|
|
3824
3847
|
============================================================ */
|
|
@@ -3849,7 +3872,7 @@
|
|
|
3849
3872
|
transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
|
|
3850
3873
|
}
|
|
3851
3874
|
.ds-247420 .ds-filter-pill:hover { background: var(--bg-3); color: var(--fg); }
|
|
3852
|
-
.ds-247420 .ds-filter-pill.active { background: var(--accent-tint); color: var(--
|
|
3875
|
+
.ds-247420 .ds-filter-pill.active { background: var(--accent-tint); color: var(--fg); border-color: var(--accent); }
|
|
3853
3876
|
.ds-247420 .ds-filter-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
3854
3877
|
|
|
3855
3878
|
/* Touch floor for the new small controls. */
|
|
@@ -4907,6 +4930,16 @@
|
|
|
4907
4930
|
border-radius: var(--r-1);
|
|
4908
4931
|
}
|
|
4909
4932
|
|
|
4933
|
+
/* ThreadPanel / Forum / Page interactive elements */
|
|
4934
|
+
.ds-247420 .cm-tp-item:focus-visible,
|
|
4935
|
+
.ds-247420 .cm-forum-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--r-1); }
|
|
4936
|
+
.ds-247420 .cm-tp-new:focus-visible,
|
|
4937
|
+
.ds-247420 .cm-tp-close:focus-visible,
|
|
4938
|
+
.ds-247420 .cm-forum-new:focus-visible,
|
|
4939
|
+
.ds-247420 .cm-forum-sort:focus-visible,
|
|
4940
|
+
.ds-247420 .cm-page-edit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-1); }
|
|
4941
|
+
.ds-247420 .cm-forum-search:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); }
|
|
4942
|
+
|
|
4910
4943
|
/* ---------- mobile header ---------- */
|
|
4911
4944
|
.ds-247420 .cm-mobile-header {
|
|
4912
4945
|
display: flex;
|
|
@@ -5487,7 +5520,7 @@
|
|
|
5487
5520
|
background: none;
|
|
5488
5521
|
border: 1px solid var(--rule);
|
|
5489
5522
|
color: inherit;
|
|
5490
|
-
border-radius:
|
|
5523
|
+
border-radius: var(--r-1);
|
|
5491
5524
|
padding: 2px 8px;
|
|
5492
5525
|
cursor: pointer;
|
|
5493
5526
|
font: inherit;
|
|
@@ -5506,6 +5539,7 @@
|
|
|
5506
5539
|
}
|
|
5507
5540
|
.ds-247420 .agentchat-cwd-input:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }
|
|
5508
5541
|
.ds-247420 .agentchat-cwd-input[aria-invalid='true'] { border-color: var(--flame); box-shadow: inset 0 0 0 var(--bw-hair) var(--flame); }
|
|
5542
|
+
.ds-247420 .agentchat-cwd-input[aria-busy='true'] { border-color: var(--fg-3); box-shadow: inset 0 0 0 var(--bw-hair) var(--fg-3); }
|
|
5509
5543
|
|
|
5510
5544
|
/* head + thread */
|
|
5511
5545
|
.ds-247420 .agentchat-head {
|
|
@@ -5756,18 +5790,14 @@
|
|
|
5756
5790
|
}
|
|
5757
5791
|
.ds-247420 .ds-session-new > span { display: none; }
|
|
5758
5792
|
.ds-247420 .ds-session-new:hover { background: var(--bg-2); color: var(--fg); }
|
|
5759
|
-
|
|
5793
|
+
/* Rail filter uses the shared .ds-search-input primitive (provides bg/border/
|
|
5794
|
+
radius/focus-ring); only the rail layout + touch floor live here. */
|
|
5795
|
+
.ds-247420 .ds-session-head .ds-search-input { order: 1; flex: 1 1 auto; min-width: 0; }
|
|
5760
5796
|
.ds-247420 .ds-session-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
5761
|
-
.ds-247420 .ds-session-search {
|
|
5762
|
-
width: 100%; padding: var(--space-1) var(--space-3); min-height: 36px;
|
|
5763
|
-
background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); color: var(--fg);
|
|
5764
|
-
border-radius: var(--r-1); font-family: var(--ff-body); font-size: var(--fs-sm);
|
|
5765
|
-
}
|
|
5766
|
-
.ds-247420 .ds-session-search:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); }
|
|
5767
5797
|
/* Touch floor (must FOLLOW the base rules - same specificity, order decides). */
|
|
5768
5798
|
@media (pointer: coarse) {
|
|
5769
5799
|
.ds-247420 .ds-session-new { width: 44px; min-height: 44px; }
|
|
5770
|
-
.ds-247420 .ds-session-search { min-height: 44px; }
|
|
5800
|
+
.ds-247420 .ds-session-head .ds-search-input { min-height: 44px; }
|
|
5771
5801
|
}
|
|
5772
5802
|
.ds-247420 .ds-session-list, .ds-247420 .ds-session-groups { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-2); }
|
|
5773
5803
|
/* Grouped rows (Today/Yesterday/...) lay out like the flat list; the section
|
|
@@ -5946,7 +5976,7 @@
|
|
|
5946
5976
|
border-radius: var(--r-1); background: var(--bg-2); color: var(--fg-2);
|
|
5947
5977
|
cursor: pointer; font-family: var(--ff-body); font-size: var(--fs-tiny);
|
|
5948
5978
|
}
|
|
5949
|
-
.ds-247420 .ds-dash-errors-toggle.active { background: var(--accent-tint); color: var(--
|
|
5979
|
+
.ds-247420 .ds-dash-errors-toggle.active { background: var(--accent-tint); color: var(--fg); border-color: var(--accent); }
|
|
5950
5980
|
.ds-247420 .ds-dash-errors-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
5951
5981
|
|
|
5952
5982
|
/* --- C3: per-card select checkbox. --- */
|
|
@@ -6114,6 +6144,7 @@
|
|
|
6114
6144
|
/* Inline cwd validation line (checking / error) under the cwd input. */
|
|
6115
6145
|
.ds-247420 .agentchat-cwd-hint { font-size: var(--fs-tiny); color: var(--fg-3); }
|
|
6116
6146
|
.ds-247420 .agentchat-cwd-hint.is-error { color: var(--flame); }
|
|
6147
|
+
.ds-247420 .agentchat-cwd-hint.is-checking { color: var(--fg-2); font-style: italic; }
|
|
6117
6148
|
|
|
6118
6149
|
/* Dashboard: shared session title heading (same string as the rails). */
|
|
6119
6150
|
.ds-247420 .ds-dash-title {
|
|
@@ -6213,6 +6244,7 @@
|
|
|
6213
6244
|
}
|
|
6214
6245
|
.ds-247420 .chat-msg .chat-tool .chat-tool-head::-webkit-details-marker { display: none; }
|
|
6215
6246
|
.ds-247420 .chat-msg .chat-tool .chat-tool-head:hover { background: var(--bg-2); }
|
|
6247
|
+
.ds-247420 .chat-msg .chat-tool .chat-tool-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
6216
6248
|
.ds-247420 .chat-tool-icon { display: inline-flex; color: var(--fg-3); }
|
|
6217
6249
|
.ds-247420 .chat-tool-name { font-family: var(--ff-mono); font-weight: 600; color: var(--fg); }
|
|
6218
6250
|
.ds-247420 .chat-tool-label { color: var(--fg-3); font-size: var(--fs-tiny); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
@@ -6233,6 +6265,7 @@
|
|
|
6233
6265
|
.ds-247420 .chat-tool-section-label { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--fg-3); }
|
|
6234
6266
|
.ds-247420 .chat-tool-copy { position: static; opacity: 0; }
|
|
6235
6267
|
.ds-247420 .chat-tool-section:hover .chat-tool-copy, .ds-247420 .chat-tool-section:focus-within .chat-tool-copy { opacity: 1; }
|
|
6268
|
+
.ds-247420 .chat-tool-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }
|
|
6236
6269
|
.ds-247420 .chat-tool-pre { margin: 0; padding: var(--space-2); background: var(--bg-2); border-radius: var(--r-1); font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1.45; overflow-x: auto; max-height: 320px; overflow-y: auto; }
|
|
6237
6270
|
.ds-247420 .chat-tool-pre.is-error { color: var(--flame); }
|
|
6238
6271
|
.ds-247420 .chat-tool-pre.chat-tool-empty { color: var(--fg-3); }
|
|
@@ -6288,15 +6321,17 @@
|
|
|
6288
6321
|
.ds-247420 .ds-dash-breakdown .seg { display: inline-flex; align-items: center; gap: var(--space-1); font-weight: 600; }
|
|
6289
6322
|
.ds-247420 .ds-dash-breakdown .seg::before { content: ''; width: 7px; height: 7px; border-radius: 50%; flex: none; }
|
|
6290
6323
|
.ds-247420 .ds-dash-breakdown .seg.is-running { color: var(--accent); }
|
|
6291
|
-
.ds-247420 .ds-dash-breakdown .seg.is-running::before { background: var(--accent); }
|
|
6324
|
+
.ds-247420 .ds-dash-breakdown .seg.is-running::before { background: var(--accent); box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--accent) 30%, transparent); }
|
|
6292
6325
|
.ds-247420 .ds-dash-breakdown .seg.is-error { color: var(--flame); }
|
|
6293
6326
|
.ds-247420 .ds-dash-breakdown .seg.is-error::before { background: var(--flame); box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--flame) 38%, transparent); }
|
|
6294
|
-
.ds-247420 .ds-dash-breakdown .seg.is-idle { color: var(--
|
|
6295
|
-
.ds-247420 .ds-dash-breakdown .seg.is-idle::before { background: transparent; box-shadow: inset 0 0 0
|
|
6327
|
+
.ds-247420 .ds-dash-breakdown .seg.is-idle { color: var(--stale); }
|
|
6328
|
+
.ds-247420 .ds-dash-breakdown .seg.is-idle::before { background: transparent; box-shadow: inset 0 0 0 1px var(--stale), inset 0 0 0 3px var(--bg); }
|
|
6296
6329
|
.ds-247420 .ds-dash-stream-disc { display: inline-flex; align-items: center; gap: var(--space-1); }
|
|
6297
6330
|
.ds-247420 .ds-dash-selectall { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--fs-tiny); color: var(--fg-2); cursor: pointer; background: none; border: none; padding: var(--space-1); }
|
|
6298
6331
|
.ds-247420 .ds-dash-selectall:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
6299
6332
|
.ds-247420 .ds-dash-clear { background: none; border: none; color: var(--fg-3); cursor: pointer; font-size: var(--fs-tiny); text-decoration: underline dotted; padding: var(--space-1); }
|
|
6333
|
+
.ds-247420 .ds-dash-clear:hover { color: var(--fg); }
|
|
6334
|
+
.ds-247420 .ds-dash-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: var(--r-1); }
|
|
6300
6335
|
|
|
6301
6336
|
/* Conversation-rail loading skeleton (cold ccsniff walk). */
|
|
6302
6337
|
.ds-247420 .ds-session-row-skeleton { display: flex; flex-direction: column; gap: 6px; padding: var(--space-2) var(--space-3); }
|
|
@@ -7504,7 +7539,7 @@
|
|
|
7504
7539
|
.ds-247420 .ds-input-check:checked { background: var(--accent); border-color: var(--accent); }
|
|
7505
7540
|
.ds-247420 .ds-input-check:checked::after {
|
|
7506
7541
|
content: ''; position: absolute; left: 4px; top: 1px;
|
|
7507
|
-
width: 4px; height: 8px; border: solid var(--accent-fg
|
|
7542
|
+
width: 4px; height: 8px; border: solid var(--accent-fg);
|
|
7508
7543
|
border-width: 0 2px 2px 0; transform: rotate(45deg);
|
|
7509
7544
|
}
|
|
7510
7545
|
.ds-247420 .ds-input-check:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
@@ -7546,7 +7581,7 @@
|
|
|
7546
7581
|
-webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
|
|
7547
7582
|
border: 1px solid var(--rule);
|
|
7548
7583
|
border-radius: var(--r-2, 8px);
|
|
7549
|
-
box-shadow:
|
|
7584
|
+
box-shadow: var(--shadow-3);
|
|
7550
7585
|
color: var(--panel-text);
|
|
7551
7586
|
overflow: hidden;
|
|
7552
7587
|
}
|
|
@@ -7870,9 +7905,6 @@
|
|
|
7870
7905
|
.ds-247420 .ds-event-list .row[role="button"]:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); }
|
|
7871
7906
|
.ds-247420 .ds-event-list .row.event-flash { animation: agentgui-event-flash 2s ease-out; }
|
|
7872
7907
|
|
|
7873
|
-
/* Chat composer: hide the idle scrollbar on the (empty/short) textarea. */
|
|
7874
|
-
.ds-247420 .chat-composer textarea { overflow-y: auto; scrollbar-width: thin; }
|
|
7875
|
-
.ds-247420 .chat-composer textarea:not(:focus) { overflow-y: hidden; }
|
|
7876
7908
|
|
|
7877
7909
|
/* Generic interactive focus ring for app-emitted controls. */
|
|
7878
7910
|
.ds-247420 button:focus-visible,
|
|
@@ -7913,6 +7945,10 @@
|
|
|
7913
7945
|
}
|
|
7914
7946
|
|
|
7915
7947
|
@media print {
|
|
7948
|
+
/* Re-assert the paper-tuned signal tokens so token-derived foregrounds
|
|
7949
|
+
(status discs, error text, tool cards) print with light-theme colours
|
|
7950
|
+
rather than the dark-theme-derived values inherited under auto-dark. */
|
|
7951
|
+
.ds-247420 { --flame:#C53E00; --amber:#8A6512; --warn:#E0241A; --sky:#3A6EFF; --bg:var(--paper); --fg:var(--ink); }
|
|
7916
7952
|
.ds-247420 #app { min-height: auto; display: block; height: auto; }
|
|
7917
7953
|
.ds-247420 .skip-link, .ds-247420 .status-dot, .ds-247420 .history-actions, .ds-247420 .chat-composer { display: none !important; }
|
|
7918
7954
|
.ds-247420 .app, .ds-247420 .app-main, .ds-247420 .panel, .ds-247420 .chat, .ds-247420 .chat-thread {
|
|
@@ -7930,20 +7966,28 @@
|
|
|
7930
7966
|
.ds-247420 .site-panel { margin: var(--space-2); }
|
|
7931
7967
|
|
|
7932
7968
|
/* Hero block */
|
|
7933
|
-
.ds-247420 .site-hero { padding:
|
|
7969
|
+
.ds-247420 .site-hero { padding: var(--space-4); }
|
|
7934
7970
|
.ds-247420 .site-hero-h { margin: 0 0 var(--space-2); }
|
|
7935
7971
|
.ds-247420 .site-hero-body { margin: var(--space-2) 0 var(--space-3); color: var(--fg-2); max-width: 64ch; }
|
|
7936
7972
|
.ds-247420 .site-chip-row { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 var(--space-3); }
|
|
7937
7973
|
.ds-247420 .site-cta-row { display: flex; gap: var(--space-2); flex-wrap: wrap; }
|
|
7938
7974
|
|
|
7939
7975
|
/* Quickstart CLI block — these .cli/.prompt/.cmd nodes were previously unstyled. */
|
|
7940
|
-
.ds-247420 .site-cli { padding:
|
|
7941
|
-
.ds-247420 .site-cli .cli { display: flex; gap: var(--space-2); font-family: var(--ff-mono); font-size: var(--fs-sm); padding: 2px 0; }
|
|
7976
|
+
.ds-247420 .site-cli { padding: var(--space-3); background: var(--bg); border: var(--bw-hair, 1px) solid var(--rule); border-radius: var(--r-1); }
|
|
7977
|
+
.ds-247420 .site-cli .cli { display: flex; align-items: baseline; gap: var(--space-2); font-family: var(--ff-mono); font-size: var(--fs-sm); padding: 2px 0; }
|
|
7942
7978
|
.ds-247420 .site-cli .prompt { color: var(--fg-3); user-select: none; flex: 0 0 auto; }
|
|
7943
7979
|
.ds-247420 .site-cli .cmd { color: var(--fg); white-space: pre-wrap; word-break: break-word; }
|
|
7944
7980
|
|
|
7945
|
-
/* Embedded legacy doc (iframe wrapper)
|
|
7946
|
-
|
|
7981
|
+
/* Embedded legacy doc (iframe wrapper). 180px = the page chrome (topbar +
|
|
7982
|
+
* crumb + footer) sitting above the embed; dvh keeps it mobile-safe. */
|
|
7983
|
+
.ds-247420 .site-embed { width: 100%; height: calc(100dvh - 180px); min-height: 320px; border: 0; border-radius: var(--r-1); background: var(--bg-2); display: block; }
|
|
7984
|
+
|
|
7985
|
+
/* Marketing footer family (the in-app .app-status strip suppresses content
|
|
7986
|
+
* below 1100px; the site footer always shows its credits row). */
|
|
7987
|
+
.ds-247420 .site-footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); width: 100%; padding: var(--space-3) var(--space-4); font-family: var(--ff-body); font-size: var(--fs-sm); line-height: 1.4; color: var(--fg-3); border-top: 1px solid var(--rule); }
|
|
7988
|
+
.ds-247420 .site-footer .item { color: inherit; }
|
|
7989
|
+
.ds-247420 .site-footer .item:first-of-type { color: var(--accent); }
|
|
7990
|
+
.ds-247420 .site-footer .spread { flex: 1; }
|
|
7947
7991
|
|
|
7948
7992
|
/* spoint/loading-screen.css */
|
|
7949
7993
|
/* Loading-screen kit styles. Scoped under .ds-247420 at build time.
|