agentgui 1.0.1059 → 1.0.1061
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/.gm/memories/mem-166f52796185e0e7-1487.md +8 -0
- package/.gm/memories/mem-28889e43e8ce64cc-3663.md +8 -0
- package/.gm/prd.yml +30 -0
- package/AGENTS.md +1 -1
- package/lib/http-handler.js +27 -13
- package/package.json +1 -1
- package/site/app/js/app.js +148 -19
- package/site/app/js/backend.js +21 -7
- package/site/app/vendor/anentrypoint-design/247420.css +63 -10
- package/site/app/vendor/anentrypoint-design/247420.js +15 -15
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-166f52796185e0e7-1487
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783891644525
|
|
5
|
+
updated: 1783891644525
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{"key":"agentgui-27th-run-negative-space-attention-detail","value":"agentgui 27th run (2026-07-03): negative-space + attention sweep, scoped to the specific ask (not a full agent fan-out) - a live-browser review (screenshots + DOM measurement at 1280px and 420px) across chat/files/settings. Chat/settings visual balance was already reasonable from the prior 26 runs; found and fixed 2 real issues. (1) Files filter placeholder 'Filter files in this directory' clipped against its intentionally-compact 220px input (no ellipsis, last character cut off at the box edge) - shortened to 'Filter files', which fits without widening the control (the kit's own comment already states the compact width is deliberate: a full-width filter 'reads as a search page and costs fold height'). (2) Settings SERVER panel rendered 4 short facts (version/uptime/connected clients/projects folder) each as its own .lede div - .lede is the fs-xl/tall-line-height INTRO-PARAGRAPH style, not a metadata style, so 4 short facts read as 4 oversized paragraphs with real dead vertical space between them, and inconsistent density vs the allowed-roots rows directly below (already using the compact SessionMeta strip). Folded all 4 facts into one SessionMeta call alongside the roots - same information, denser, one consistent style for every fact in the panel. Browser-witnessed at both viewport widths, no wrap/overlap regression. No kit changes needed (both fixes were app-only copy/wiring). Pushed b0b38fb."}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-28889e43e8ce64cc-3663
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783891625733
|
|
5
|
+
updated: 1783891625733
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{"key":"agentgui-41st-run-gui-logic-predictability-sweep","value":"agentgui 41st run (2026-07-12): gui-logic-predictability workflow wf_c645ba95-c6a (23 agents, 192 tool calls, 0 errors) - 10 confirmed findings, all implemented, plus one real kit bug found+fixed mid-verification. App (site/app/js/app.js): runningPanel title retitled 'all running sessions · N' (was scoping-ambiguous 'running · N') plus a '(refreshing…)' suffix while a backgrounded-tab poll catches up (new state._activeStale flag); runningRowTitle() gained the same 3s 'indexing…' carve-out computeLiveSessions() already had, so a brand-new session no longer reads Untitled in one panel and indexing in another; Live dashboard elapsed/activity sort now freezes to the last-computed order while a pointer is held down anywhere in the dashboard (new module-level _livePointerDown flag) so cards stop reordering under a mid-click; agy gained a composer disclosure ('continues most recent conversation') matching claude-code's resume banner; History's ConversationList mapper now reads a real agent/model badge sourced from ccsniff (previously hardcoded agent:undefined). Kit (design/src/components/{chat,content}.js, chat.css, sessions.js): ChatMessage's per-message copy action gained the same label/icon click-feedback flip its sibling copy controls already had; .agentchat-followup chips gained a dashed border to visually distinguish contextual follow-ups from generic empty-state seed chips; flashComposerNote's single shared DOM node now queues notes instead of silently overwriting an unread one; SessionDashboard's filter SearchInput now forwards resultCount to its aria-live region - wiring this up SURFACED a real pre-existing bug: SearchInput conditionally returned either a bare <input> or a wrapping <span> depending on whether resultCount/clearBtn were set, and since resultCount now toggles between renders, webjsx's applyDiff corrupted the DOM (a merged select+input node, the real input vanishing entirely) - fixed by always returning the .ds-search-input-wrap shape (confirmed display:contents, layout-transparent, safe everywhere). ccsniff (separate repo, github:AnEntrypoint/ccsniff#main, cloned to /config/workspace/ccsniff-work): Store.sessions() folds in the most-recent-by-ts model per session; index.js's assistant-event ingestion never actually populated model on pushed blocks (only system/init events carried it) - fixed to inherit message.model from the real Claude Code JSONL shape. node test.js ALL TESTS PASS both ccsniff commits (11f6c3b, then 26bbfcc/beff5e7 after a remote-advanced rebase), CI green. Reinstalling required deleting bun.lock entirely (a --force reinstall alone kept resolving to the stale pinned git sha even after rm -rf node_modules/ccsniff). The local buildesk server process needed an explicit SIGTERM restart for the new node_modules/ccsniff to load (Node/Bun caches modules in-process) - the env's custom /opt/gmweb-startup supervisor auto-respawned it within ~20s via bunx agentgui@latest, which correctly resolves to the local workspace's own bin/gmgui.cjs (workspace-linked, confirmed via the 'Workspace mode: skipping npm version checker' startup log line), not the published npm package. Kit pushed 71ee4e4, agentgui pushed 7ad7b4f then b32836e (PRD-resolve residual), all CI green all three repos (agentgui/design/ccsniff). Browser-witnessed every finding live on buildesk: Live filter input/select intact + working post-fix, aria-live '0 results'; .chat-msg-action.is-copied/.agentchat-followup CSS rules live in CSSOM; History rows reading real agent+model badges (18/18 sessions, claude-sonnet-5); 0 pageErrors throughout."}
|
package/.gm/prd.yml
CHANGED
|
@@ -3076,3 +3076,33 @@
|
|
|
3076
3076
|
- gm-plugkit/playwriter browser verb dom=/capture eval context defect - requires an upstream tool fix outside agentgui/design
|
|
3077
3077
|
status: completed
|
|
3078
3078
|
witness: 'External tool defect, not resolvable from this repo/session: the gm-plugkit/playwriter browser verb''s dom= and capture prefixes evaluate in a Node vm context with no page/document binding (ReferenceError on every attempt this session), unlike the screenshot= and url=<target> paths which do reach the real page. This is a defect in the upstream gm-plugkit/playwriter tool package itself, outside agentgui and design repo scope. Recorded per the false-completion rule as blockedBy:external rather than marked done - screenshots remain the working witness fallback and were used for all browser verification in this sweep.'
|
|
3079
|
+
- id: cwd-validation-realtime-existing-probe-review
|
|
3080
|
+
subject: Audit the existing debouncedCwdProbe/onCwdDraft validation - confirm it still races correctly with the new browse/recent/autocomplete affordances (no double state writes, no stale error surviving a valid pick)
|
|
3081
|
+
status: pending
|
|
3082
|
+
- id: practicality-sweep-composer-ergonomics
|
|
3083
|
+
subject: 'Beyond cwd: sweep the chat composer and immediate surrounding controls for other practicality gaps (agent/model switching friction, draft persistence edge cases, file attach friction) via a workflow dispatch'
|
|
3084
|
+
status: pending
|
|
3085
|
+
- id: practicality-sweep-files-surface
|
|
3086
|
+
subject: Sweep the Files tab for practicality gaps beyond the cwd-picker integration (navigation friction, bulk-op discoverability, missing shortcuts)
|
|
3087
|
+
status: pending
|
|
3088
|
+
- id: practicality-sweep-history-settings
|
|
3089
|
+
subject: Sweep History and Settings surfaces for practicality gaps (search ergonomics, settings organization, discoverability of power-user features)
|
|
3090
|
+
status: pending
|
|
3091
|
+
- id: practicality-sweep-live-dashboard
|
|
3092
|
+
subject: Sweep the Live dashboard for remaining practicality gaps beyond the 40th/41st run fixes
|
|
3093
|
+
status: pending
|
|
3094
|
+
- id: practicality-verify-buildesk-live
|
|
3095
|
+
subject: Live-verify all cwd-practicality and general-practicality findings on https://buildesk.acc.l-inc.co.za/gm via browser witness
|
|
3096
|
+
status: pending
|
|
3097
|
+
- id: practicality-kit-build-vendor
|
|
3098
|
+
subject: Build+test the design kit, re-vendor dist into site/app/vendor/anentrypoint-design
|
|
3099
|
+
status: pending
|
|
3100
|
+
- id: practicality-push-ci-green
|
|
3101
|
+
subject: Push kit and agentgui changes, verify CI green on both repos
|
|
3102
|
+
status: pending
|
|
3103
|
+
- id: practicality-agents-md-record
|
|
3104
|
+
subject: Record this run's findings in AGENTS.md following the existing run-log convention
|
|
3105
|
+
status: pending
|
|
3106
|
+
- id: docstudio-cue-session-group-eyebrow
|
|
3107
|
+
subject: Verify ConversationList's session-group headers (Today/Yesterday/etc) use the --tr-label token + --fg-3 tone consistently, matching docstudio's uppercase letter-spaced low-opacity eyebrow label idiom
|
|
3108
|
+
status: pending
|
package/AGENTS.md
CHANGED
|
@@ -58,7 +58,7 @@ Screenshot audit fixed 3 "looks very diy" causes: `.ws-sessions` flat `--bg` (sh
|
|
|
58
58
|
|
|
59
59
|
## Negative-space + attention sweep (2026-07-03) — twenty-seventh run
|
|
60
60
|
|
|
61
|
-
Scoped
|
|
61
|
+
Scoped live-browser review (chat/files/settings, 1280px+420px); fixed a clipped Files filter placeholder and a misapplied `.lede` styling 4 settings facts as oversized paragraphs. App-only, pushed `b0b38fb`. Full detail in rs-learn (recall "agentgui 27th run negative-space-attention detail").
|
|
62
62
|
|
|
63
63
|
## Per-row file move + lint tooling fix (2026-07-02) — twenty-sixth run
|
|
64
64
|
|
package/lib/http-handler.js
CHANGED
|
@@ -84,6 +84,25 @@ function sanitizeEntryName(name) {
|
|
|
84
84
|
return n;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Every confined filesystem route accepts its target path via a `?path=`/
|
|
88
|
+
// `?dir=` query param, preferred over a path SEGMENT. A reverse proxy's
|
|
89
|
+
// proxy_pass URI normalization can decode-then-reencode the request path,
|
|
90
|
+
// collapsing an encoded %2F segment slash back into a literal '/' before
|
|
91
|
+
// forwarding - the app then sees extra path segments instead of one opaque
|
|
92
|
+
// one, strips what it thinks is a leading '/', and silently turns an absolute
|
|
93
|
+
// path into a relative one that fails confinement even for genuinely
|
|
94
|
+
// accessible directories/files. A query param is untouched by that
|
|
95
|
+
// normalization on any proxy in front of this app. `legacyPrefix` is the old
|
|
96
|
+
// `/api/xxx/<path>` route prefix, still accepted for any caller not yet
|
|
97
|
+
// updated to the query-param form.
|
|
98
|
+
function resolveConfinedPath(req, routePath, queryKey, legacyPrefix) {
|
|
99
|
+
const url = new URL(req.url, 'http://x');
|
|
100
|
+
const qVal = url.searchParams.get(queryKey);
|
|
101
|
+
if (qVal != null) return qVal;
|
|
102
|
+
const raw = routePath.split('?')[0].slice(legacyPrefix.length).replace(/^\//, '');
|
|
103
|
+
return raw ? decodeURIComponent(raw) : '';
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
// Map a Node.js filesystem error code to a safe human-readable string that
|
|
88
107
|
// does not disclose host paths or internal stack context. Used everywhere an
|
|
89
108
|
// err.message would otherwise be returned to the client.
|
|
@@ -382,8 +401,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
382
401
|
// to the same allowlist as the Files surface (an unconfined stat would be
|
|
383
402
|
// a filesystem oracle). Returns {ok, dir} or a 403/404 with plain copy.
|
|
384
403
|
if (routePath.startsWith('/api/stat') && req.method === 'GET') {
|
|
385
|
-
const
|
|
386
|
-
const decodedPath = rawS ? decodeURIComponent(rawS) : '';
|
|
404
|
+
const decodedPath = resolveConfinedPath(req, routePath, 'path', '/api/stat');
|
|
387
405
|
const conf = confineToRoots(decodedPath, fsAllowRoots());
|
|
388
406
|
if (!conf.ok) { sendJSON(req, res, conf.reason === 'not found' ? 404 : 403, { error: conf.reason }); return; }
|
|
389
407
|
try {
|
|
@@ -441,8 +459,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
441
459
|
// FileGrid + BreadcrumbPath render directly. Allowed roots default to the
|
|
442
460
|
// server cwd + Claude projects dir; widen via FS_ROOTS (path-separated).
|
|
443
461
|
if (routePath.startsWith('/api/list')) {
|
|
444
|
-
const
|
|
445
|
-
const decodedPath = rawQ ? decodeURIComponent(rawQ) : '';
|
|
462
|
+
const decodedPath = resolveConfinedPath(req, routePath, 'dir', '/api/list');
|
|
446
463
|
const allowRoots = fsAllowRoots();
|
|
447
464
|
// Empty path = the first allow-root (a sane default landing dir).
|
|
448
465
|
const reqPath = !decodedPath ? allowRoots[0] : decodedPath;
|
|
@@ -509,9 +526,8 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
509
526
|
// and limited to reasonable text/code/image types so this is never a
|
|
510
527
|
// generic arbitrary-file reader. Images are served via /api/image; this
|
|
511
528
|
// returns text/* with utf-8.
|
|
512
|
-
if (routePath.startsWith('/api/file/')) {
|
|
513
|
-
const
|
|
514
|
-
const decodedPath = decodeURIComponent(rawF);
|
|
529
|
+
if (routePath.startsWith('/api/file/') || routePath.startsWith('/api/file?')) {
|
|
530
|
+
const decodedPath = resolveConfinedPath(req, routePath, 'path', '/api/file/');
|
|
515
531
|
const allowRoots = fsAllowRoots();
|
|
516
532
|
const conf = confineToRoots(decodedPath, allowRoots);
|
|
517
533
|
if (!conf.ok) { res.writeHead(conf.reason === 'not found' ? 404 : 403); res.end('Forbidden'); return; }
|
|
@@ -556,9 +572,8 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
556
572
|
// Confined raw-bytes download (any type) with an attachment disposition,
|
|
557
573
|
// so the Files view can offer download on a row. Same allowlist + realpath
|
|
558
574
|
// confinement as /api/file and /api/image - never a generic file reader.
|
|
559
|
-
if (routePath.startsWith('/api/download/')) {
|
|
560
|
-
const
|
|
561
|
-
const decodedPath = decodeURIComponent(rawD);
|
|
575
|
+
if (routePath.startsWith('/api/download/') || routePath.startsWith('/api/download?')) {
|
|
576
|
+
const decodedPath = resolveConfinedPath(req, routePath, 'path', '/api/download/');
|
|
562
577
|
const allowRoots = fsAllowRoots();
|
|
563
578
|
const conf = confineToRoots(decodedPath, allowRoots);
|
|
564
579
|
if (!conf.ok) { res.writeHead(conf.reason === 'not found' ? 404 : 403); res.end('Forbidden'); return; }
|
|
@@ -766,9 +781,8 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
766
781
|
return;
|
|
767
782
|
}
|
|
768
783
|
|
|
769
|
-
if (routePath.startsWith('/api/image/')) {
|
|
770
|
-
const
|
|
771
|
-
const decodedPath = decodeURIComponent(imagePath);
|
|
784
|
+
if (routePath.startsWith('/api/image/') || routePath.startsWith('/api/image?')) {
|
|
785
|
+
const decodedPath = resolveConfinedPath(req, routePath, 'path', '/api/image/');
|
|
772
786
|
// Confine reads to an allowlist root. Without this the route is an
|
|
773
787
|
// arbitrary-file-read of any image-extensioned path on the host (the
|
|
774
788
|
// prior `includes('..')` guard is a no-op after path.normalize resolves
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -1665,6 +1665,21 @@ function filesMain() {
|
|
|
1665
1665
|
// stop-selected against sessions that no longer exist.
|
|
1666
1666
|
const LIVE_PREFS_KEY = 'agentgui.live';
|
|
1667
1667
|
const FILES_PREFS_KEY = 'agentgui.files';
|
|
1668
|
+
// Recent-cwd MRU: a small, most-recently-used list of working directories
|
|
1669
|
+
// actually saved, so switching between a regular handful of projects needs no
|
|
1670
|
+
// re-typing/re-browsing. Capped small - this is a quick-pick convenience, not
|
|
1671
|
+
// a full history (that's what the Files tab + browse popover are for).
|
|
1672
|
+
const CWD_RECENT_KEY = 'agentgui.cwd.recent';
|
|
1673
|
+
const CWD_RECENT_CAP = 6;
|
|
1674
|
+
function loadRecentCwds() {
|
|
1675
|
+
try { const v = JSON.parse(lsGet(CWD_RECENT_KEY) || '[]'); return Array.isArray(v) ? v : []; } catch { return []; }
|
|
1676
|
+
}
|
|
1677
|
+
function pushRecentCwd(path) {
|
|
1678
|
+
if (!path) return;
|
|
1679
|
+
const list = loadRecentCwds().filter((p) => p !== path);
|
|
1680
|
+
list.unshift(path);
|
|
1681
|
+
lsSet(CWD_RECENT_KEY, JSON.stringify(list.slice(0, CWD_RECENT_CAP)));
|
|
1682
|
+
}
|
|
1668
1683
|
function persistLivePrefs() {
|
|
1669
1684
|
lsSet(LIVE_PREFS_KEY, JSON.stringify({ sort: state.live.sort || 'status', errorsOnly: !!state.live.errorsOnly, filter: state.live.filter || '' }));
|
|
1670
1685
|
if (state.tab === 'live') writeHash();
|
|
@@ -2296,6 +2311,16 @@ function chatMain() {
|
|
|
2296
2311
|
cwdDraft: state.cwdDraft,
|
|
2297
2312
|
cwdError: state.cwdError || null,
|
|
2298
2313
|
cwdChecking: !!state.cwdChecking,
|
|
2314
|
+
// Practicality upgrade: allowed roots (one-click starting points, always
|
|
2315
|
+
// visible instead of buried in the Files tab's own picker), a small
|
|
2316
|
+
// recent-cwd MRU (one-click switch between the handful of directories a
|
|
2317
|
+
// user actually works in), and an inline browse popover (click through
|
|
2318
|
+
// subdirectories right here instead of round-tripping through Files).
|
|
2319
|
+
cwdRoots: (Array.isArray(state.files.roots) && state.files.roots.length)
|
|
2320
|
+
? state.files.roots.map((r) => ({ path: r, label: truncate(projectLabel(r) || r, 14, 24) }))
|
|
2321
|
+
: undefined,
|
|
2322
|
+
cwdRecent: loadRecentCwds().filter((p) => p !== state.chatCwd),
|
|
2323
|
+
cwdBrowse: state.cwdBrowse || undefined,
|
|
2299
2324
|
// Pasted images upload through the same confined endpoint the Files tab
|
|
2300
2325
|
// uses, into the chat's current cwd, then insert the resulting relative
|
|
2301
2326
|
// path into the draft - matching the desktop-composer expectation
|
|
@@ -2332,14 +2357,35 @@ function chatMain() {
|
|
|
2332
2357
|
if (was !== now) render();
|
|
2333
2358
|
},
|
|
2334
2359
|
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
2335
|
-
onCwdEdit: () => {
|
|
2360
|
+
onCwdEdit: () => {
|
|
2361
|
+
state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; state.cwdError = null; render();
|
|
2362
|
+
requestAnimationFrame(() => { const inp = document.querySelector('.agentchat-cwd-input'); if (inp) inp.focus(); });
|
|
2363
|
+
// A fresh user has no way to discover what's even browsable if roots
|
|
2364
|
+
// are only fetched on the first "browse" click - prefetch them the
|
|
2365
|
+
// moment the editor opens (root-only listDir call, cheap) so the
|
|
2366
|
+
// always-visible roots row is populated on the very first open.
|
|
2367
|
+
if (!(state.files.roots && state.files.roots.length)) {
|
|
2368
|
+
B.listDir(state.backend, '').then((j) => { state.files.roots = j.roots || []; render(); }).catch(() => {});
|
|
2369
|
+
}
|
|
2370
|
+
},
|
|
2371
|
+
onCwdBrowseToggle: () => toggleCwdBrowse(),
|
|
2372
|
+
onCwdBrowseCrumb: (i) => cwdBrowseCrumb(i),
|
|
2373
|
+
onCwdBrowseEnter: (path) => loadCwdBrowseDir(path),
|
|
2374
|
+
onCwdBrowsePick: (path) => cwdBrowsePick(path),
|
|
2336
2375
|
onCwdSave: async () => {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
//
|
|
2340
|
-
//
|
|
2341
|
-
|
|
2342
|
-
|
|
2376
|
+
let path = (state.cwdDraft ?? '').trim();
|
|
2377
|
+
const isAbsolute = /^([/\\]|[A-Za-z]:[/\\])/.test(path);
|
|
2378
|
+
// A bare relative subpath (no leading / or drive letter) resolves
|
|
2379
|
+
// against the CURRENT chat cwd, not the server process dir - lets a
|
|
2380
|
+
// power user type a known subfolder name instead of the full path or
|
|
2381
|
+
// opening the browse popover. Only meaningful when a cwd is already
|
|
2382
|
+
// set; with no current cwd there's no sensible base to resolve against.
|
|
2383
|
+
if (path && !isAbsolute && state.chatCwd) {
|
|
2384
|
+
const base = state.chatCwd.replace(/[/\\]+$/, '');
|
|
2385
|
+
const sep = state.chatCwd.includes('\\') && !state.chatCwd.includes('/') ? '\\' : '/';
|
|
2386
|
+
path = base + sep + path.replace(/^\.[/\\]/, '');
|
|
2387
|
+
} else if (path && !isAbsolute) {
|
|
2388
|
+
state.cwdError = 'enter an absolute path (e.g. /home/you/proj or C:\\proj), a subfolder name (resolves against the current cwd once one is set), or leave blank';
|
|
2343
2389
|
render();
|
|
2344
2390
|
return;
|
|
2345
2391
|
}
|
|
@@ -2350,7 +2396,7 @@ function chatMain() {
|
|
|
2350
2396
|
if (!st || st.ok === false) { state.cwdError = 'directory not found on the server: ' + path; render(); return; }
|
|
2351
2397
|
if (!st.dir) { state.cwdError = 'that path is not a directory'; render(); return; }
|
|
2352
2398
|
} catch (e) {
|
|
2353
|
-
state.cwdError = e.status === 403 ? 'outside the accessible folders'
|
|
2399
|
+
state.cwdError = e.status === 403 ? 'outside the accessible folders - use "browse…" or a "recent"/root chip above to pick a reachable one'
|
|
2354
2400
|
: (e.status === 404 ? 'directory not found on the server: ' + path
|
|
2355
2401
|
: (e.status ? 'directory not found on the server: ' + path : 'could not validate the path - server unreachable'));
|
|
2356
2402
|
render();
|
|
@@ -2359,16 +2405,66 @@ function chatMain() {
|
|
|
2359
2405
|
}
|
|
2360
2406
|
state.cwdError = null;
|
|
2361
2407
|
state.chatCwd = path;
|
|
2362
|
-
if (state.chatCwd) lsSet('agentgui.cwd', state.chatCwd); else lsRemove('agentgui.cwd');
|
|
2363
|
-
state.cwdEditing = false; state.cwdDraft = undefined; render();
|
|
2408
|
+
if (state.chatCwd) { lsSet('agentgui.cwd', state.chatCwd); pushRecentCwd(state.chatCwd); } else lsRemove('agentgui.cwd');
|
|
2409
|
+
state.cwdEditing = false; state.cwdDraft = undefined; state.cwdBrowse = null; render();
|
|
2364
2410
|
},
|
|
2365
|
-
onCwdCancel: () => { state.cwdEditing = false; state.cwdDraft = undefined; state.cwdError = null; state.cwdChecking = false; render(); requestAnimationFrame(() => { const btn = document.querySelector('.agentchat-cwd-btn'); if (btn) btn.focus(); }); },
|
|
2411
|
+
onCwdCancel: () => { state.cwdEditing = false; state.cwdDraft = undefined; state.cwdError = null; state.cwdChecking = false; state.cwdBrowse = null; render(); requestAnimationFrame(() => { const btn = document.querySelector('.agentchat-cwd-btn'); if (btn) btn.focus(); }); },
|
|
2366
2412
|
onCwdClear: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); },
|
|
2367
2413
|
onCwdDraft: (v) => { state.cwdDraft = v; state.cwdError = null; debouncedCwdProbe(); },
|
|
2368
2414
|
}),
|
|
2369
2415
|
].filter(Boolean);
|
|
2370
2416
|
}
|
|
2371
2417
|
|
|
2418
|
+
// Inline cwd-browse popover: a lightweight directory-only listing reusing the
|
|
2419
|
+
// same confined B.listDir the Files tab uses, but writing to its own
|
|
2420
|
+
// state.cwdBrowse rather than state.files - opening/closing the cwd browser
|
|
2421
|
+
// must never disturb whatever the Files tab currently has loaded, and vice
|
|
2422
|
+
// versa (independent request-id guards against cross-talk if both are open).
|
|
2423
|
+
let _cwdBrowseReqId = 0;
|
|
2424
|
+
async function loadCwdBrowseDir(dirPath) {
|
|
2425
|
+
const myReq = (_cwdBrowseReqId += 1);
|
|
2426
|
+
state.cwdBrowse = { ...(state.cwdBrowse || {}), loading: true };
|
|
2427
|
+
render();
|
|
2428
|
+
try {
|
|
2429
|
+
const j = await B.listDir(state.backend, dirPath || '');
|
|
2430
|
+
if (_cwdBrowseReqId !== myReq) return;
|
|
2431
|
+
state.cwdBrowse = {
|
|
2432
|
+
current: j.path,
|
|
2433
|
+
segments: j.segments || [],
|
|
2434
|
+
rootLabel: (state.files.roots && state.files.roots.length > 1) ? 'roots' : 'root',
|
|
2435
|
+
// Directory-only listing: the cwd picker is for choosing a folder to run
|
|
2436
|
+
// in, not for browsing/opening files - filter out non-directory entries.
|
|
2437
|
+
entries: (j.entries || []).filter((e) => e.type === 'dir' || e.isDir || e.dir),
|
|
2438
|
+
loading: false,
|
|
2439
|
+
};
|
|
2440
|
+
render();
|
|
2441
|
+
} catch (e) {
|
|
2442
|
+
if (_cwdBrowseReqId !== myReq) return;
|
|
2443
|
+
state.cwdBrowse = { ...(state.cwdBrowse || {}), loading: false, entries: [] };
|
|
2444
|
+
render();
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
function toggleCwdBrowse() {
|
|
2448
|
+
if (state.cwdBrowse) { state.cwdBrowse = null; render(); return; }
|
|
2449
|
+
loadCwdBrowseDir(state.cwdDraft || state.chatCwd || '');
|
|
2450
|
+
}
|
|
2451
|
+
function cwdBrowseCrumb(segIdx) {
|
|
2452
|
+
if (!state.cwdBrowse || !state.cwdBrowse.segments) return;
|
|
2453
|
+
const target = segIdx === 0 ? '' : state.cwdBrowse.segments.slice(0, segIdx).join('/');
|
|
2454
|
+
loadCwdBrowseDir(target);
|
|
2455
|
+
}
|
|
2456
|
+
// "use this folder" commits the browse popover's CURRENT directory into the
|
|
2457
|
+
// draft input (leaving the popover open state cleared) so the user still
|
|
2458
|
+
// confirms via the normal save flow (which re-validates + persists to recent).
|
|
2459
|
+
function cwdBrowsePick(path) {
|
|
2460
|
+
if (!path) return;
|
|
2461
|
+
state.cwdDraft = path;
|
|
2462
|
+
state.cwdError = null;
|
|
2463
|
+
state.cwdBrowse = null;
|
|
2464
|
+
debouncedCwdProbe();
|
|
2465
|
+
render();
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2372
2468
|
function offlineBanner() {
|
|
2373
2469
|
if (state.health.status === 'ok' || state.health.status === 'unknown') return null;
|
|
2374
2470
|
return Alert({ key: 'offline', kind: 'error', title: 'Backend unreachable',
|
|
@@ -2805,19 +2901,31 @@ async function sendChat(textArg) {
|
|
|
2805
2901
|
// Validate the cwd draft while editing (debounced) so an invalid path reads as
|
|
2806
2902
|
// invalid before the save click, via the existing confined /api/stat endpoint.
|
|
2807
2903
|
const debouncedCwdProbe = debounce(async () => {
|
|
2808
|
-
const
|
|
2904
|
+
const raw = (state.cwdDraft ?? '').trim();
|
|
2809
2905
|
if (!state.cwdEditing) return;
|
|
2810
|
-
|
|
2906
|
+
const isAbsolute = /^([/\\]|[A-Za-z]:[/\\])/.test(raw);
|
|
2907
|
+
let path = raw;
|
|
2908
|
+
if (raw && !isAbsolute && state.chatCwd) {
|
|
2909
|
+
// Mirror onCwdSave's relative-subpath resolution so the live-typing hint
|
|
2910
|
+
// reflects the same interpretation the save button will actually apply.
|
|
2911
|
+
const base = state.chatCwd.replace(/[/\\]+$/, '');
|
|
2912
|
+
const sep = state.chatCwd.includes('\\') && !state.chatCwd.includes('/') ? '\\' : '/';
|
|
2913
|
+
path = base + sep + raw.replace(/^\.[/\\]/, '');
|
|
2914
|
+
} else if (raw && !isAbsolute) {
|
|
2915
|
+
state.cwdChecking = false; render(); return; // no cwd to resolve against yet - onCwdSave surfaces the error on save
|
|
2916
|
+
} else if (!raw) {
|
|
2917
|
+
state.cwdChecking = false; render(); return;
|
|
2918
|
+
}
|
|
2811
2919
|
state.cwdChecking = true; render();
|
|
2812
|
-
const probed =
|
|
2920
|
+
const probed = raw; // compare against the RAW (unresolved) draft to detect staleness
|
|
2813
2921
|
try {
|
|
2814
|
-
const st = await B.statPath(state.backend,
|
|
2922
|
+
const st = await B.statPath(state.backend, path); // query the RESOLVED absolute path
|
|
2815
2923
|
if ((state.cwdDraft ?? '').trim() !== probed) return; // draft moved on
|
|
2816
2924
|
state.cwdError = (!st || st.ok === false) ? 'folder not found on the server'
|
|
2817
2925
|
: (!st.dir ? 'that path is not a directory' : null);
|
|
2818
2926
|
} catch (e) {
|
|
2819
2927
|
if ((state.cwdDraft ?? '').trim() !== probed) return;
|
|
2820
|
-
state.cwdError = e.status === 403 ? 'outside the accessible folders'
|
|
2928
|
+
state.cwdError = e.status === 403 ? 'outside the accessible folders - try "browse…" or a chip above'
|
|
2821
2929
|
: (e.status === 404 ? 'folder not found on the server' : null);
|
|
2822
2930
|
}
|
|
2823
2931
|
state.cwdChecking = false;
|
|
@@ -2966,7 +3074,9 @@ function historyMain() {
|
|
|
2966
3074
|
}, { turns: 0, tools: 0, errors: 0 });
|
|
2967
3075
|
const meta = SessionMeta({
|
|
2968
3076
|
items: [
|
|
2969
|
-
sess && sess.cwd ? { label: 'directory', value: sess.cwd, title: sess.cwd
|
|
3077
|
+
sess && sess.cwd ? { label: 'directory', value: sess.cwd, title: sess.cwd,
|
|
3078
|
+
actionLabel: 'use as chat cwd',
|
|
3079
|
+
onAction: () => { state.chatCwd = sess.cwd; lsSet('agentgui.cwd', sess.cwd); pushRecentCwd(sess.cwd); announce('working directory set to ' + sess.cwd); render(); } } : null,
|
|
2970
3080
|
(() => { const dur = sessionDuration(); return dur ? { label: 'duration', value: dur } : null; })(),
|
|
2971
3081
|
{ label: 'session id', value: state.selectedSid.slice(0, 8) + '…', title: state.selectedSid, onCopy: () => copyText(state.selectedSid, 'session id copied') },
|
|
2972
3082
|
// Spelled counter vocabulary in the detail strip (events/turns/tools/
|
|
@@ -4041,8 +4151,16 @@ window.addEventListener('keydown', (e) => {
|
|
|
4041
4151
|
}
|
|
4042
4152
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
4043
4153
|
if (typing) {
|
|
4044
|
-
|
|
4045
|
-
|
|
4154
|
+
// The cwd editor's own text input is the one exception: Escape there must
|
|
4155
|
+
// close the editor in a single press (matching every other Escape-
|
|
4156
|
+
// closeable surface in the app), not blur-then-require-a-second-press.
|
|
4157
|
+
if (e.key === 'Escape' && t.classList && t.classList.contains('agentchat-cwd-input')) {
|
|
4158
|
+
t.blur();
|
|
4159
|
+
// Fall through to the ladder below instead of returning early.
|
|
4160
|
+
} else {
|
|
4161
|
+
if (e.key === 'Escape') t.blur();
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4046
4164
|
}
|
|
4047
4165
|
if (e.key === 'Escape') {
|
|
4048
4166
|
// Priority ladder for transient state (modals/drawers are kit-handled):
|
|
@@ -4052,6 +4170,17 @@ window.addEventListener('keydown', (e) => {
|
|
|
4052
4170
|
// backdrop listener covers in-dialog focus; this covers everything else).
|
|
4053
4171
|
if (state.files.dialog) { if (!state.files.dialog.busy) closeFileDialog(); return; }
|
|
4054
4172
|
if (state.chat.confirmingEdit) { state.chat.confirmingEdit = null; render(); announce('edit cancelled'); return; }
|
|
4173
|
+
// cwd editor: the browse popover is a nested layer within it - Escape
|
|
4174
|
+
// closes the popover first (one level of the nesting) before falling
|
|
4175
|
+
// through to closing the whole editor on a second press, matching the
|
|
4176
|
+
// dialog-then-page Escape convention used elsewhere in the app.
|
|
4177
|
+
if (state.cwdEditing) {
|
|
4178
|
+
if (state.cwdBrowse) { state.cwdBrowse = null; render(); announce('folder browser closed'); return; }
|
|
4179
|
+
state.cwdEditing = false; state.cwdDraft = undefined; state.cwdError = null; state.cwdChecking = false;
|
|
4180
|
+
render(); announce('cwd edit cancelled');
|
|
4181
|
+
requestAnimationFrame(() => { const btn = document.querySelector('.agentchat-cwd-btn'); if (btn) btn.focus(); });
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4055
4184
|
if (state.confirmingClearData) { state.confirmingClearData = false; render(); announce('clear cancelled'); return; }
|
|
4056
4185
|
if (state.confirmingNewChat) { clearTimeout(_newChatArmTimer); state.confirmingNewChat = false; render(); announce('new chat cancelled'); return; }
|
|
4057
4186
|
if (state.live.confirmingStopAll || state.live.confirmingStopSelected) {
|
package/site/app/js/backend.js
CHANGED
|
@@ -91,8 +91,16 @@ export async function probeBackend(base) {
|
|
|
91
91
|
// An empty path lands on the server's first allow-root. The server confines
|
|
92
92
|
// every path to an allowlist root, so a 403 means the path is out of bounds.
|
|
93
93
|
export async function listDir(base, dirPath) {
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
// A path SEGMENT (encoded as %2F for its own slashes) is silently mangled by
|
|
95
|
+
// some reverse-proxy configs that normalize proxy_pass URIs - a nginx
|
|
96
|
+
// decode-then-reencode of the request path collapses %2F back into a literal
|
|
97
|
+
// slash before forwarding, so the app receives extra path segments instead
|
|
98
|
+
// of one opaque one, loses its leading slash on strip, and 403s every
|
|
99
|
+
// absolute-path listing that isn't the bare root. A query param is never
|
|
100
|
+
// subject to that URI-path normalization, so it survives any proxy in front
|
|
101
|
+
// of this app untouched.
|
|
102
|
+
const qs = dirPath ? '?dir=' + encodeURIComponent(dirPath) : '';
|
|
103
|
+
const r = await authedFetch(base + '/api/list' + qs);
|
|
96
104
|
const j = await r.json().catch(() => ({}));
|
|
97
105
|
if (!r.ok) {
|
|
98
106
|
// Carry the HTTP status so the app can map 403/404 to plain copy (W9).
|
|
@@ -108,7 +116,10 @@ export async function listDir(base, dirPath) {
|
|
|
108
116
|
// a 403 means out-of-bounds or unsupported type, 404 missing. Returns
|
|
109
117
|
// { content, truncated }.
|
|
110
118
|
export async function readFile(base, filePath) {
|
|
111
|
-
|
|
119
|
+
// Query param, not a path segment - see listDir()'s comment above for why:
|
|
120
|
+
// a reverse proxy's URI normalization can silently collapse an encoded %2F
|
|
121
|
+
// path-segment slash back into a literal slash before forwarding.
|
|
122
|
+
const r = await authedFetch(base + '/api/file?path=' + encodeURIComponent(filePath));
|
|
112
123
|
if (!r.ok) {
|
|
113
124
|
const t = await r.text().catch(() => '');
|
|
114
125
|
throw new Error(t || ('file: ' + r.status));
|
|
@@ -119,13 +130,13 @@ export async function readFile(base, filePath) {
|
|
|
119
130
|
|
|
120
131
|
// Same-origin URL for an image file (served by /api/image, token-threaded).
|
|
121
132
|
export function imageUrl(base, filePath) {
|
|
122
|
-
return withToken(base + '/api/image
|
|
133
|
+
return withToken(base + '/api/image?path=' + encodeURIComponent(filePath));
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
// Same-origin download URL (served by /api/download, attachment disposition,
|
|
126
137
|
// confined + token-threaded). Used by the Files row download action.
|
|
127
138
|
export function downloadUrl(base, filePath) {
|
|
128
|
-
return withToken(base + '/api/download
|
|
139
|
+
return withToken(base + '/api/download?path=' + encodeURIComponent(filePath));
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
// ---------- File mutations (confined server endpoints) ----------
|
|
@@ -143,10 +154,13 @@ async function mutateJSON(base, route, body) {
|
|
|
143
154
|
return j;
|
|
144
155
|
}
|
|
145
156
|
|
|
146
|
-
// Stat a path (GET /api/stat
|
|
157
|
+
// Stat a path (GET /api/stat?path=<encoded>) for cwd validation. Returns {ok, dir}.
|
|
147
158
|
// Throws an Error carrying .status (403 outside roots, 404 missing).
|
|
148
159
|
export async function statPath(base, p) {
|
|
149
|
-
|
|
160
|
+
// Query param, not a path segment - see the comment in listDir() above for
|
|
161
|
+
// why: a reverse proxy's URI normalization can silently collapse an encoded
|
|
162
|
+
// %2F path-segment slash back into a literal slash before forwarding.
|
|
163
|
+
const r = await authedFetch(base + '/api/stat?path=' + encodeURIComponent(p));
|
|
150
164
|
const j = await r.json().catch(() => ({}));
|
|
151
165
|
if (!r.ok) { const e = new Error(j.error || ('stat: ' + r.status)); e.status = r.status; throw e; }
|
|
152
166
|
return j;
|
|
@@ -60,8 +60,14 @@
|
|
|
60
60
|
--sky: #3A6EFF;
|
|
61
61
|
/* --warn = interactive danger controls (buttons, banners, menus, error
|
|
62
62
|
inputs); --danger below = passive error/status text and metrics
|
|
63
|
-
(kpi/sparkline deltas, auth-error text). Two tokens, one convention.
|
|
64
|
-
|
|
63
|
+
(kpi/sparkline deltas, auth-error text). Two tokens, one convention.
|
|
64
|
+
#E0241A measured 4.19:1 on --paper — fails WCAG AA 4.5:1 for the many
|
|
65
|
+
TEXT usages (.field-error, .ds-btn-warn, .ds-stat-val.err-rate, toast/
|
|
66
|
+
session error labels), not just the interactive-control (>=3:1) usages.
|
|
67
|
+
Darkened to clear AA on --paper (5.29:1), --paper-2 (4.78:1), and the
|
|
68
|
+
thebird warm-paper preset (5.25:1) while staying visually a clear
|
|
69
|
+
warm red. */
|
|
70
|
+
--warn: #C41C0C;
|
|
65
71
|
/* Syntax-token aliases: light values pass 4.5:1 on paper; the dark blocks
|
|
66
72
|
restore the bright dark-tuned originals. Components reference ONLY these. */
|
|
67
73
|
--code-string: var(--green);
|
|
@@ -477,7 +483,7 @@
|
|
|
477
483
|
--accent: var(--acid); --accent-fg: var(--ink); --accent-ink: var(--acid-deep);
|
|
478
484
|
--accent-tint: color-mix(in oklab, var(--accent-ink) 22%, var(--bg));
|
|
479
485
|
/* A paper island under a dark ancestor must not inherit the dark signal pair. */
|
|
480
|
-
--flame: #C53E00; --amber: #7C570F; --warn: #
|
|
486
|
+
--flame: #C53E00; --amber: #7C570F; --warn: #C41C0C; --sky: #3A6EFF;
|
|
481
487
|
}
|
|
482
488
|
|
|
483
489
|
/* thebird — warm-paper brand preset. A first-class swappable theme: it lives
|
|
@@ -951,6 +957,22 @@
|
|
|
951
957
|
.ds-247420 .app-main > .ds-files-stack,
|
|
952
958
|
.ds-247420 .app-main > .grow { flex: 1 1 auto; min-height: 0; }
|
|
953
959
|
.ds-247420 .app-main.narrow { max-width: var(--measure-narrow); margin: 0 auto; }
|
|
960
|
+
/* Article-body heading rhythm and lede spacing, consumed by flatspace-rendered
|
|
961
|
+
long-form pages (docs, papers) that drop headings/panels directly into .app-main. */
|
|
962
|
+
.ds-247420 .app-main > h1,
|
|
963
|
+
.ds-247420 .app-main > h2,
|
|
964
|
+
.ds-247420 .app-main > h3 { margin-top: 36px; margin-bottom: 12px; }
|
|
965
|
+
.ds-247420 .app-main > h1:first-child { margin-top: 8px; }
|
|
966
|
+
.ds-247420 .app-main > .panel,
|
|
967
|
+
.ds-247420 .app-main > .work-detail-chips,
|
|
968
|
+
.ds-247420 .app-main > .cli { margin-top: 18px; margin-bottom: 18px; }
|
|
969
|
+
.ds-247420 .app-main .ds-lede { margin-top: 4px; margin-bottom: 18px; max-width: 64ch; line-height: 1.6; }
|
|
970
|
+
.ds-247420 .work-detail-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; row-gap: 10px; }
|
|
971
|
+
@media (max-width: 720px) {
|
|
972
|
+
.ds-247420 .app-main > h1,
|
|
973
|
+
.ds-247420 .app-main > h2,
|
|
974
|
+
.ds-247420 .app-main > h3 { margin-top: 28px; }
|
|
975
|
+
}
|
|
954
976
|
|
|
955
977
|
@media (min-width: 1400px) {
|
|
956
978
|
/* Fluid ceiling, not a hard stop: scales from 1400px up to 1920px across the
|
|
@@ -7077,6 +7099,37 @@
|
|
|
7077
7099
|
.ds-247420 .agentchat-cwd-input[aria-invalid='true'] { border-color: var(--flame); box-shadow: inset 0 0 0 var(--bw-hair) var(--flame); }
|
|
7078
7100
|
.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); }
|
|
7079
7101
|
|
|
7102
|
+
/* cwd practicality upgrade: editing mode stacks the input row plus any
|
|
7103
|
+
optional roots/recent/browse rows the host wires, instead of a single flat
|
|
7104
|
+
row - each sub-row wraps independently at narrow widths. */
|
|
7105
|
+
.ds-247420 .agentchat-cwd-editing { flex-direction: column; align-items: stretch; }
|
|
7106
|
+
.ds-247420 .agentchat-cwd-row { display: flex; align-items: center; gap: var(--space-2, 8px); flex-wrap: wrap; }
|
|
7107
|
+
.ds-247420 .agentchat-cwd-roots, .ds-247420 .agentchat-cwd-recent {
|
|
7108
|
+
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
|
7109
|
+
}
|
|
7110
|
+
.ds-247420 .agentchat-cwd-recent-label { font-size: var(--fs-tiny); color: var(--fg-3); }
|
|
7111
|
+
.ds-247420 .agentchat-cwd-chip {
|
|
7112
|
+
background: var(--bg-2); border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
|
|
7113
|
+
border-radius: var(--r-pill); padding: 3px 10px; font: inherit; font-size: var(--fs-tiny);
|
|
7114
|
+
cursor: pointer;
|
|
7115
|
+
}
|
|
7116
|
+
.ds-247420 .agentchat-cwd-chip:hover { border-color: var(--accent); color: var(--fg); }
|
|
7117
|
+
.ds-247420 .agentchat-cwd-chip:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
|
|
7118
|
+
.ds-247420 .agentchat-cwd-browse {
|
|
7119
|
+
border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
|
|
7120
|
+
padding: var(--space-2, 8px); background: var(--bg-2);
|
|
7121
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
7122
|
+
max-height: 220px;
|
|
7123
|
+
}
|
|
7124
|
+
.ds-247420 .agentchat-cwd-browse-list { overflow-y: auto; display: flex; flex-direction: column; max-height: 160px; }
|
|
7125
|
+
.ds-247420 .agentchat-cwd-browse-item {
|
|
7126
|
+
background: none; border: none; color: var(--fg-2); text-align: left;
|
|
7127
|
+
padding: 6px 8px; border-radius: var(--r-1); font: inherit; font-size: var(--fs-tiny);
|
|
7128
|
+
cursor: pointer;
|
|
7129
|
+
}
|
|
7130
|
+
.ds-247420 .agentchat-cwd-browse-item:hover, .ds-247420 .agentchat-cwd-browse-item:focus-visible { background: var(--bg-3, var(--bg)); color: var(--fg); }
|
|
7131
|
+
.ds-247420 .agentchat-cwd-browse-loading, .ds-247420 .agentchat-cwd-browse-empty { padding: 6px 8px; font-size: var(--fs-tiny); color: var(--fg-3); }
|
|
7132
|
+
|
|
7080
7133
|
/* head + thread */
|
|
7081
7134
|
.ds-247420 .agentchat-head {
|
|
7082
7135
|
display: flex;
|
|
@@ -7277,7 +7330,7 @@
|
|
|
7277
7330
|
}
|
|
7278
7331
|
/* Coarse-pointer (touch): raise cwd button to 44px minimum tap target */
|
|
7279
7332
|
@media (pointer: coarse) {
|
|
7280
|
-
.ds-247420 .agentchat-cwd-btn { min-height: 44px; }
|
|
7333
|
+
.ds-247420 .agentchat-cwd-btn, .ds-247420 .agentchat-cwd-chip, .ds-247420 .agentchat-cwd-browse-item { min-height: 44px; }
|
|
7281
7334
|
}
|
|
7282
7335
|
|
|
7283
7336
|
/* ----------------------------------------------------------------------------
|
|
@@ -7363,7 +7416,7 @@
|
|
|
7363
7416
|
label sticks to the top of the scroll area for Claude-Desktop-style headers. */
|
|
7364
7417
|
.ds-247420 .ds-session-group { display: flex; flex-direction: column; }
|
|
7365
7418
|
.ds-247420 .ds-session-group-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
7366
|
-
.ds-247420 .ds-session-group-label { position: sticky; top: 0; z-index: 1; background: var(--bg-1, var(--bg)); font-size: var(--fs-tiny); font-weight: 600; color: var(--fg-3); text-transform: uppercase; letter-spacing: var(--tr-
|
|
7419
|
+
.ds-247420 .ds-session-group-label { position: sticky; top: 0; z-index: 1; background: var(--bg-1, var(--bg)); font-size: var(--fs-tiny); font-weight: 600; color: var(--fg-3); text-transform: uppercase; letter-spacing: var(--tr-label); padding: var(--space-2) var(--space-2) var(--space-1); }
|
|
7367
7420
|
.ds-247420 .ds-session-row {
|
|
7368
7421
|
position: relative; display: flex; align-items: center; gap: var(--space-2);
|
|
7369
7422
|
width: 100%; padding: var(--space-2); min-height: 52px; margin-bottom: 2px;
|
|
@@ -7660,15 +7713,15 @@
|
|
|
7660
7713
|
.ds-247420 .ds-session-meta-item + .ds-session-meta-item::before { content: '\00B7'; color: var(--fg-3); margin-right: var(--space-1); }
|
|
7661
7714
|
.ds-247420 .ds-session-meta-label { color: var(--fg-3); }
|
|
7662
7715
|
.ds-247420 .ds-session-meta-value { font-family: var(--ff-mono); color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
7663
|
-
.ds-247420 .ds-session-meta-copy {
|
|
7716
|
+
.ds-247420 .ds-session-meta-copy, .ds-247420 .ds-session-meta-action {
|
|
7664
7717
|
padding: 2px 8px; min-height: 24px; cursor: pointer;
|
|
7665
7718
|
background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1);
|
|
7666
7719
|
color: var(--fg-3); font-family: var(--ff-body); font-size: var(--fs-micro);
|
|
7667
7720
|
}
|
|
7668
|
-
.ds-247420 .ds-session-meta-copy:hover { background: var(--bg-3); color: var(--fg); }
|
|
7669
|
-
.ds-247420 .ds-session-meta-copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
|
|
7721
|
+
.ds-247420 .ds-session-meta-copy:hover, .ds-247420 .ds-session-meta-action:hover { background: var(--bg-3); color: var(--fg); }
|
|
7722
|
+
.ds-247420 .ds-session-meta-copy:focus-visible, .ds-247420 .ds-session-meta-action:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
|
|
7670
7723
|
@media (pointer: coarse), (max-width: 640px) {
|
|
7671
|
-
.ds-247420 .ds-session-meta-copy { min-height: 44px; min-width: 44px; }
|
|
7724
|
+
.ds-247420 .ds-session-meta-copy, .ds-247420 .ds-session-meta-action { min-height: 44px; min-width: 44px; }
|
|
7672
7725
|
}
|
|
7673
7726
|
|
|
7674
7727
|
/* ============================================================================
|
|
@@ -7917,7 +7970,7 @@
|
|
|
7917
7970
|
|
|
7918
7971
|
/* Live command-center: status-bucket groups + header breakdown + heartbeat. */
|
|
7919
7972
|
.ds-247420 .ds-dash-group { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
7920
|
-
.ds-247420 .ds-dash-group-label { font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-
|
|
7973
|
+
.ds-247420 .ds-dash-group-label { font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-label); color: var(--fg-3); padding: 0 var(--space-1); }
|
|
7921
7974
|
.ds-247420 .ds-dash-breakdown { display: flex; align-items: center; gap: var(--space-1); font-size: var(--fs-sm); color: var(--fg-2); }
|
|
7922
7975
|
/* Shape-channel discs mirror the .status-dot-disc vocabulary (solid /
|
|
7923
7976
|
solid+halo / hollow ring) so a colour-blind user can scan the breakdown by
|