agentgui 1.0.1041 → 1.0.1043
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/.flat-export-done +1 -1
- package/.gm/memories/mem-231ff5de003f20f0-586.md +8 -0
- package/.gm/memories/mem-455b3beb4afabe14-591.md +8 -0
- package/.gm/memories/mem-4845ee594759151c-560.md +8 -0
- package/.gm/memories/mem-8f59884039787ef3-743.md +8 -0
- package/.gm/memories/mem-993ab31f7a10aefa-718.md +8 -0
- package/.gm/memories/mem-a3e440db285f655a-995.md +8 -0
- package/.gm/memories/mem-ade01d9c02677c55-1557.md +8 -0
- package/.gm/memories/mem-dd98b21c0896dc04-1557.md +8 -0
- package/.gm/memories/mem-e490a181de6469fc-487.md +8 -0
- package/.gm/memories/mem-f9f0dc8895b21e18-1217.md +8 -0
- package/.gm/memories/mem-fe3faa1652f07239-480.md +8 -0
- package/AGENTS.md +6 -4
- package/package.json +1 -1
- package/site/app/js/app.js +137 -25
- package/site/app/vendor/anentrypoint-design/247420.css +166 -13
- package/site/app/vendor/anentrypoint-design/247420.js +14 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"digest":"
|
|
1
|
+
{"digest":"fc5b1d9e8eb6b62a:n=7","total":7}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-231ff5de003f20f0-586
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783842773069
|
|
5
|
+
updated: 1783842773069
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{"claim":"All grants for opened docs + derived/created docs are pre-accepted","scope":"feedback","why":"Tiffany hit SCOPE_NEEDED on sheets_read because URL-heuristic grant fired for converted /spreadsheets/d/ URL but her OAuth lacked sheets scope. User rule: opening a doc implies all access needed to work with it AND any derivative is approved upfront.","witness_evidence":"public/js/streaming/acp-client.js DOC_TYPE_CATEGORIES broadened to read+write across doc-domain; created docs auto-tracked via docAccess.addCreated in functions/streaming/modules/anthropic-gemini-proxy.js:129"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-455b3beb4afabe14-591
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1780592707612
|
|
5
|
+
updated: 1780592707612
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui plugin tool provisioning on Windows: lib/tool-spawner.js must iterate bun->npx fallback. BUNX_RUNNERS=['bun','npx'] tries each in sequence; on Windows hosts without bun the missing-command error arrives via stderr+close (not the 'error' event), so isMissingCmdError regex matches /not recognized|ENOENT|command not found|cannot find/i on BOTH error.message and captured stdout+stderr. Only falls through to next runner when missing flag set. Pattern: binary-might-not-exist -> runner fallback + check both error.message and process output streams + cross-platform regex alternation.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-4845ee594759151c-560
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1780592706754
|
|
5
|
+
updated: 1780592706754
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui WebSocket /sync endpoint testing: server sends sync_connected with clientId on connect. Legacy handler lib/ws-legacy-handlers.js handles ping->pong, subscribe->subscription_confirmed, get_subscriptions->subscriptions, unsubscribe, latency_report. All responses use codec encode/decode (lib/codec.js). Message ordering requires registering the ws.on('message') handler BEFORE sending. Test pattern: const queued=[]; let waiting; ws.on('message',...); queued.forEach(m=>ws.send(m)); waiting.resolve(...) — ensures handler is live before messages flow.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-8f59884039787ef3-743
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1780592686732
|
|
5
|
+
updated: 1780592686732
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui live validation target: https://buildesk.acc.l-inc.co.za/gm — nginx basic-auth user 'abc' password '123,slam,123,slam' (also satisfies the app's own PASSWORD gate). App listens PORT=9897 BASE_URL=/gm behind nginx (proxy_pass 127.0.0.1:9897). For the gm browser verb, use the DEFAULT session (NOT 'session new <name>' which uses an extension bridge that cannot reach external HTTPS) and set page.setExtraHTTPHeaders Authorization Basic (embedding creds in the URL breaks the app's fetch('/health') with 'URL that includes credentials'). Static site/app edits go live immediately (etag-keyed asset cache, no restart). Playwright reuses page across goto so clearCookies/about:blank to defeat session caching when verifying fresh edits.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-993ab31f7a10aefa-718
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1780593487331
|
|
5
|
+
updated: 1780593487331
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui DS loading: index.html loads anentrypoint-design from unpkg @latest (247420.css + 247420.js) at RUNTIME, not the local vendor/ copy. Deliberate (commit 947086e) because the chat surface uses the load-bearing AgentChat kit component which exists in @latest (0.0.186) but NOT in the older locally-vendored bundle. So: GUI needs network, shifts on upstream publish, local vendoring bypassed, and the vendored bundle's si/ri/_s jsdelivr markdown imports (marked@15/dompurify@3/prismjs) are moot while on unpkg. To restore offline: re-vendor 0.0.186+ (has AgentChat) + re-localize (gzip Ha blob, fonts, CDN consts) + rewire si/ri/_s to ../cdn/ + point index.html back to ./vendor/. PRD row revendor-ds-186-offline.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-a3e440db285f655a-995
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1779979137157
|
|
5
|
+
updated: 1779979137157
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui GUI styling (site/app): The anentrypoint-design DS bundle (vendor/anentrypoint-design/247420.js) exposes CSS vars --ff-display (='Inter, system-ui' in this bundle version — NOT Space Grotesk) and --ff-mono (='JetBrains Mono'); there is NO --font-sans. index.html body must use var(--ff-display). CRITICAL CASCADE GOTCHA: installStyles() injects the DS bundle CSS into a runtime <style data-247420> appended AFTER index.html's head <style>, so equal-specificity local overrides LOSE on source order. To override a DS component rule (.app-topbar nav a.active, .chat-head .sub, focus rings, etc) you must out-specify the .ds-247420-prefixed DS selector OR use !important. The offline vendoring mechanism (gzip Ha CSS blob in 247420.js + 3 CDN string consts Us/Za/_a) is still valid. DS Chat component ignores its sub prop and renders its own zero-padded '00 msgs' count in .chat-head .sub — hide via CSS. EventList rows are div.row[role=button] (click-to-expand) inside .ds-event-list.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-ade01d9c02677c55-1557
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783699218939
|
|
5
|
+
updated: 1783699218939
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{"key":"agentgui-36th-run-ai-tell-sweep","value":"agentgui 36th run (2026-07-10): follow-on AI-tell sweep after the 35th run. gui-ai-tell-sweep workflow wf_a53b7d8f-27b (26 agents, 5 hunt lenses, 20 findings -> 11 confirmed / 9 correctly refuted). Kit fixes: context-menu/toast shadow -> --shadow-overlay token; barchart/upload/bar meter fills --accent -> --fg-3 (routine metering shouldnt use the loudest brand hue); checkbox checked-fill -> --fg; drag-over triple-accent-stack (tint+border+ring) collapsed to single dashed border; skip-to-main link de-loudened from CTA-accent to neutral fg/bg; session-unread dot neutralized off --accent; dead .empty-state child rules + unused .ds-panel-trio/.ds-panel-grid hover-lift deleted; AgentChat empty-state copy de-boilerplated. App: keyboardPanel/preferencesPanel gained kind:wide for settings panel-weight hierarchy. Browser-witnessed live (0 pageErrors, CSS rules confirmed post-fix). Kit pushed 4de1771, agentgui pushed e6dc1d9, CI green both repos. IMPORTANT recurring tool-defect: gm-plugkit fs_write verb payload key is content (JSON string), NOT body -- passing {path,body:{...}} silently no-ops with bytes:0 and no error. This nearly let a stale-empty .ci-validated marker pass the COMPLETE gate this run; caught via recall mid-VERIFY (the memory already existed from a prior run) and corrected before transitioning to COMPLETE. Always use content, verify bytes>0 in the fs_write response, and spot-check the actual file content on disk before trusting a CONSOLIDATE/COMPLETE gate that depends on it."}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-dd98b21c0896dc04-1557
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783699262524
|
|
5
|
+
updated: 1783699262524
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{"key":"agentgui-26th-run-file-move-lint-fix","value":"agentgui 26th run (2026-07-02): per-row file move + lint tooling fix. Landed gui-completion #0: single-file move previously required checkbox-select + BulkBar. Kit FileRow gains a move action (between rename/delete, arrow-right icon); app onAction(move,file) seeds state.files.marked with just that one path and opens the existing bulk-move dialog, reusing runBulkMove/api/move as-is - no new dialog kind, no new server surface. Also fixed a real bug in ../design scripts/lint-null-children.mjs: its bracket-matcher did not understand // line comments, so an apostrophe inside ANY comment anywhere in a scanned file (e.g. the kit's own) was read as opening a string literal, silently corrupting bracket-depth tracking for the rest of that file - a false positive here, but potentially a false NEGATIVE elsewhere (hiding a real missing .filter(Boolean) behind an unrelated apostrophe earlier in the file). Added stripLineComments() (string-literal-aware) run before the scan; verified by reintroducing the apostrophe and confirming it now lints clean. CI note: Deploy GH Pages repeatedly failed with Deployment cancelled / Timeout reached, aborting! across this run and the 25th - GitHub Pages deployment API appears to self-cancel when a rerun is triggered while a prior attempt for the same commit SHA is still settling (rapid successive gh run rerun calls made this worse, not better). Fix: dont rerun repeatedly in quick succession - trigger once and wait it out with zero concurrent interference."}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-e490a181de6469fc-487
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783843022402
|
|
5
|
+
updated: 1783843022402
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
CORRECTION (37th run, 2026-07-12): agentgui's live index.html loads the design kit from LOCAL ./vendor/anentrypoint-design/247420.{css,js}, NOT unpkg. Witnessed directly on https://buildesk.acc.l-inc.co.za/gm/ via browser dom= query. An older memory record (mem-1780593485911-5-718) claiming unpkg @latest is STALE - it described a transient state that was reverted; AGENTS.md's 'DS load = LOCAL vendor' section is correct and current. Trust this record over the unpkg one going forward.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-f9f0dc8895b21e18-1217
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783699265581
|
|
5
|
+
updated: 1783699265581
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{"key":"agentgui-25th-run-mid-thread-retry","value":"agentgui 25th run (2026-07-02): mid-thread retry. Landed the highest-severity finding deferred from the 24th run (gui-completion #7): retry was hard-gated to the LAST message only. Kit AgentChat now offers retry on every settled assistant turn, not just i===lastIdx; onRetryMessage(m) passes the specific message. App retryTurn(m) truncates+resends from ms preceding user turn. Retrying anything before the trailing turn discards every later turn too - arms the same confirm banner edit-and-resend already uses (kind:retry tag) instead of firing immediately; retrying the actual last turn (no data loss) still executes immediately. Caught and fixed a real bug while generalizing: the old retryLastTurn() never cleared state.chat.resumeSid/resumeNote on truncation, unlike confirmEditAndResend() which already did per the never --resume a diverged tail rule - both paths now share executeTruncateAndResend() so this cant drift apart again. Kit pushed 834c81d, agentgui pushed 18cd971. Browser-witnessed end-to-end, 0 console errors, 28/28 tests. This runs Deploy GH Pages CI step flaked twice before succeeding on a 3rd rerun - infra flake, not a code regression."}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
key: mem-fe3faa1652f07239-480
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1780592683616
|
|
5
|
+
updated: 1780592683616
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui DS (anentrypoint-design 247420.js) SearchInput component (internal De) sets aria-label = (label || placeholder) and IGNORES any aria-label prop you pass. To give the history search box a real accessible name, pass label: AND placeholder: (same string). A post-render setAttribute race-loses against the DS re-render. Signature: De({value,placeholder='search…',onInput,onSubmit,name='q',key,label}). This is the SearchInput export. Witnessed live on buildesk 2026-06-04.
|
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# AgentGUI — Agent Notes
|
|
2
2
|
|
|
3
|
+
## GUI AI-tell sweep (2026-07-10) — thirty-sixth run
|
|
4
|
+
|
|
5
|
+
Follow-on to the 35th run's AI-tell sweep (commit 1702fa8). `gui-ai-tell-sweep` workflow wf_a53b7d8f-27b (26 agents, 25 done/1 verify-agent hit a mid-response connection error, 200 tool calls): 5 hunter lenses (gradient-glow-glass, emoji-generic-icons, oversaturated-color, boilerplate-copy, layout-uniformity-tells) found 20 candidates, adversarial verify confirmed 11 (9 correctly refuted as already-deliberate — status-disc pulse triplicate, `--sun` "retired" claim, radius-scale uniformity — the kept-typography-style guard doing its job on non-typography claims too). Landed in the kit (`design/app-shell.css`/`chat.css`/`src/components/agent-chat.js`): hardcoded one-off context-menu/toast shadow → `var(--shadow-overlay)` token; three data-metering fills (barchart/upload/bar) demoted from the loudest acid-lime `--accent` to neutral `--fg-3` (color reserved for state, not routine progress); checkbox checked-fill `--accent`→`--fg`; drag-over collapsed from a triple accent stack (tint+border+ring) to a single dashed border; the keyboard-only `.skip-to-main` link de-loudened from CTA-accent to neutral ink (it was visually identical to the primary send button despite being invisible to sighted users); session-unread dot neutralized off `--accent` so it stops competing with the active row's accent; dead template-shaped `.empty-state` child rules and an unused `.ds-panel-trio`/`.ds-panel-grid` hover-lift block deleted; AgentChat empty-state copy de-boilerplated away from "invitation" framing. App: `keyboardPanel()`/`preferencesPanel()` gained `kind:'wide'` so settings' purely-informational/utility panels read lighter than the consequence-bearing backend/agents panels. Kit rebuilt+re-vendored, browser-witnessed live (0 pageErrors, all 4 sampled CSS rules confirmed post-fix in the real CSSOM). Kit pushed `4de1771` (after a rebase — a concurrent version-bump commit had landed), agentgui pushed `e6dc1d9`, all CI green both repos. **Tool-defect note (recorded, not re-fixed — the workaround is durable): the gm-plugkit `fs_write` verb's payload key is `content` (a JSON string), not `body` — passing `{path, body:{...}}` silently no-ops with `bytes:0` and no error, which nearly let a stale/empty `.ci-validated` marker slip through this run; caught via `recall` mid-VERIFY and corrected before COMPLETE.**
|
|
6
|
+
|
|
3
7
|
## GUI ux-craft sweep (2026-07-10) — thirty-fourth run
|
|
4
8
|
|
|
5
9
|
`gui-ux-craft` workflow wf_919164f1-648 (34 agents, 0 errors) + a parallel live-browser negative-space audit. Full detail in rs-learn (recall "agentgui 34th run ux-craft sweep"). Headline: negative-space audit came back **clean** for the 3rd consecutive run (31st/33rd/34th) — future sweeps should shift focus to interaction-level polish instead of re-running that lens. Kit: `--fs-2`/`--fs-0` shadow font tokens didn't exist in the real scale (silently fell back to hardcoded px, decoupled from `[data-typescale]`) — remapped to `--fs-lg`/`--fs-tiny`; stale `--lh-base` fallback (1.4, should be 1.55) fixed; ~29 more hardcoded focus-ring `outline:2px solid var(--accent-ink)` instances tokenized; `.chat-tool-copy` gained the coarse-pointer 44px touch target; `.ds-select` gained a base `min-height:32px` floor; `Btn`'s `className` prop renamed to `class` to match `Panel`/`Heading`; `AgentControls`/`AgentChat` gained an `agentsLoading` state. App: History no longer falls back to the raw session id (cwd/"Untitled conversation" instead), ACP error copy truncated+prefixed. Kit pushed `3b1080c` (after a rebase — a concurrent release commit had landed), agentgui pushed `9c8094b`, all CI green both repos.
|
|
@@ -34,13 +38,11 @@ Scoped to the specific ask (negative space / attention), not a full agent fan-ou
|
|
|
34
38
|
|
|
35
39
|
## Per-row file move + lint tooling fix (2026-07-02) — twenty-sixth run
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**CI note: `Deploy GH Pages` repeatedly failed with "Deployment cancelled" / "Timeout reached, aborting!" across this run and the 25th** — GitHub Pages' deployment API appears to self-cancel when a rerun is triggered while a prior attempt for the same commit SHA is still settling (rapid successive `gh run rerun` calls made this worse, not better). Fix: don't rerun repeatedly in quick succession — trigger once (`gh run rerun` or `gh workflow run "Deploy GH Pages" --ref main` for a fresh dispatch if reruns keep reusing a wedged deployment ID) and wait it out with zero concurrent interference; both repos' deploys succeeded the moment reruns stopped racing each other. `npm publish`/`Publish and Release` (the actual consumer-facing artifacts) were green from the first attempt every time — only the docs-site GH Pages deploy flaked.
|
|
41
|
+
Single-file move action on `FileRow` reusing the bulk-move dialog; fixed a `lint-null-children.mjs` bracket-matcher bug (apostrophes in `//` comments corrupted bracket-depth tracking). Full detail in rs-learn (recall "agentgui 26th run file-move-lint-fix").
|
|
40
42
|
|
|
41
43
|
## Mid-thread retry (2026-07-02) — twenty-fifth run
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
Retry ungated from last-message-only to every settled assistant turn, with truncate-and-resend + `resumeSid` clearing shared via one `executeTruncateAndResend()` path. Full detail in rs-learn (recall "agentgui 25th run mid-thread-retry").
|
|
44
46
|
|
|
45
47
|
## GUI completion sweep (2026-07-02) — twenty-fourth run
|
|
46
48
|
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader,
|
|
6
|
+
const { AppShell, WorkspaceShell, WorkspaceRail, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, ConversationList, SessionDashboard, Row, Panel, PageHeader, TextField, Select, Btn, Icon, EventList, Spinner, Alert, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker, BreadcrumbPath, EmptyState, FileViewer, FilePreviewPane, FilePreviewCode, FilePreviewText, FilePreviewMedia, ThemeToggle, ContextPane, PromptDialog, ConfirmDialog, DropZone, UploadProgress, FilterPills, SessionMeta, BulkBar, Checkbox, ShortcutList, FocusTrap, AgentListSkeleton } = C;
|
|
7
7
|
|
|
8
8
|
// One duration/bytes vocabulary across every surface: prefer the kit's shared
|
|
9
9
|
// formatters (exported alongside the components), fall back to the local
|
|
@@ -147,6 +147,7 @@ function scheduleStreamRender() {
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
const UNTITLED_CONVERSATION = 'Untitled conversation'; // one fallback label, one casing, for every call site
|
|
150
151
|
const NARROW_BP = 640; // unified with the CSS touch-target breakpoint in index.html
|
|
151
152
|
function isNarrow() { return typeof window !== 'undefined' && window.innerWidth < NARROW_BP; }
|
|
152
153
|
function truncate(str, mobileLen, desktopLen) {
|
|
@@ -409,7 +410,15 @@ async function stopActiveChat(sid) {
|
|
|
409
410
|
if (st.has(sid)) return; // re-entry guard: one cancel per sid in flight
|
|
410
411
|
st.add(sid);
|
|
411
412
|
render();
|
|
412
|
-
try {
|
|
413
|
+
try {
|
|
414
|
+
await B.cancelChat(state.backend, sid);
|
|
415
|
+
} catch {
|
|
416
|
+
st.delete(sid);
|
|
417
|
+
state.live.bulkStopError = 'conversation did not stop - try again';
|
|
418
|
+
announce('stop failed');
|
|
419
|
+
render();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
413
422
|
// Optimistically drop the row so the card doesn't read 'running' for up to
|
|
414
423
|
// 3s after the click; the immediate refresh confirms.
|
|
415
424
|
state.active = (Array.isArray(state.active) ? state.active : []).filter(a => a.sessionId !== sid);
|
|
@@ -746,7 +755,7 @@ function sessionsColumn() {
|
|
|
746
755
|
const hits = (state.searchHits.results || []).slice(0, state.sessionsLimit);
|
|
747
756
|
const items = hits.map((r, i) => ({
|
|
748
757
|
sid: r.sid,
|
|
749
|
-
title: r.snippet || r.title ||
|
|
758
|
+
title: r.snippet || r.title || UNTITLED_CONVERSATION,
|
|
750
759
|
project: projectLabel(r.project) || '',
|
|
751
760
|
time: r.ts ? fmtRelTime(r.ts) : '',
|
|
752
761
|
running: false,
|
|
@@ -757,9 +766,14 @@ function sessionsColumn() {
|
|
|
757
766
|
return ConversationList({
|
|
758
767
|
sessions: items,
|
|
759
768
|
selected: state.selectedSid,
|
|
769
|
+
// Search runs across every project (runSearch() clears projectFilter so
|
|
770
|
+
// it doesn't silently re-narrow later) - say so, since that reset is
|
|
771
|
+
// otherwise invisible and would surprise the user when they clear the
|
|
772
|
+
// search and the session list is unexpectedly wider than before.
|
|
773
|
+
caption: 'Searching across all projects',
|
|
760
774
|
search: {
|
|
761
775
|
value: state.searchQ,
|
|
762
|
-
placeholder: 'Search conversations (2+ chars)',
|
|
776
|
+
placeholder: 'Search all conversations - titles + messages (2+ chars)',
|
|
763
777
|
onInput: (v) => { state.searchQ = v; if (v.trim().length >= 2) debouncedSearch(); else { state.searchHits = null; } render(); },
|
|
764
778
|
},
|
|
765
779
|
onNew: () => { navTo('chat'); newChat(); },
|
|
@@ -770,6 +784,8 @@ function sessionsColumn() {
|
|
|
770
784
|
loading: state.searchBusy,
|
|
771
785
|
error: state.searchHits.error || null,
|
|
772
786
|
emptyText: 'No matches for "' + state.searchQ + '"',
|
|
787
|
+
hasMore: (state.searchHits.results || []).length > state.sessionsLimit,
|
|
788
|
+
onLoadMore: () => { state.sessionsLimit += 60; render(); },
|
|
773
789
|
});
|
|
774
790
|
}
|
|
775
791
|
const sessionsView = visibleSessions();
|
|
@@ -811,7 +827,7 @@ function sessionsColumn() {
|
|
|
811
827
|
selected: state.selectedSid,
|
|
812
828
|
search: {
|
|
813
829
|
value: state.searchQ,
|
|
814
|
-
placeholder: 'Search conversations',
|
|
830
|
+
placeholder: 'Search all conversations - titles + messages',
|
|
815
831
|
onInput: (v) => { state.searchQ = v; if (v.trim().length >= 2) debouncedSearch(); else { state.searchHits = null; } render(); },
|
|
816
832
|
},
|
|
817
833
|
onNew: () => { navTo('chat'); newChat(); },
|
|
@@ -829,6 +845,8 @@ function sessionsColumn() {
|
|
|
829
845
|
loadingText: state.historySlow ? 'Indexing your Claude history — the first load can take a minute…' : undefined,
|
|
830
846
|
error: state.historyError,
|
|
831
847
|
emptyText: 'No conversations yet',
|
|
848
|
+
hasMore: sessionsView.length > state.sessionsLimit,
|
|
849
|
+
onLoadMore: () => { state.sessionsLimit += 60; render(); },
|
|
832
850
|
});
|
|
833
851
|
}
|
|
834
852
|
|
|
@@ -946,9 +964,16 @@ function fileMutationCopy(e) {
|
|
|
946
964
|
return e.message || 'The operation failed.';
|
|
947
965
|
}
|
|
948
966
|
function openFileDialog(kind, file) {
|
|
949
|
-
|
|
967
|
+
const trigger = typeof document !== 'undefined' ? document.activeElement : null;
|
|
968
|
+
state.files.dialog = { kind, file: file || null, error: null, busy: false, _trigger: trigger || null };
|
|
950
969
|
render();
|
|
951
970
|
}
|
|
971
|
+
// Restore focus to whatever row/button opened the dialog so keyboard/AT users
|
|
972
|
+
// don't lose their place to document.body on close.
|
|
973
|
+
function restoreFileDialogFocus(trigger) {
|
|
974
|
+
if (!trigger) return;
|
|
975
|
+
requestAnimationFrame(() => { if (document.contains(trigger)) trigger.focus(); });
|
|
976
|
+
}
|
|
952
977
|
// --- multi-select (marked) helpers. Marked entries are keyed by path so the
|
|
953
978
|
// set survives sort/filter changes; loadDir owns clearing/pruning it.
|
|
954
979
|
function filesMarked() {
|
|
@@ -990,14 +1015,19 @@ async function runBulkDelete() {
|
|
|
990
1015
|
if (!d || d.kind !== 'bulk-delete' || d.busy) return;
|
|
991
1016
|
const marked = filesMarked();
|
|
992
1017
|
const targets = (state.files.entries || []).filter((e) => marked.has(e.path || e.name));
|
|
993
|
-
if (!targets.length) { state.files.dialog = null; render(); return; }
|
|
1018
|
+
if (!targets.length) { state.files.dialog = null; restoreFileDialogFocus(d._trigger); render(); return; }
|
|
994
1019
|
d.busy = true; d.error = null; render();
|
|
995
1020
|
const results = await Promise.allSettled(
|
|
996
1021
|
targets.map((e) => B.deleteEntry(state.backend, e.path, e.type === 'dir')));
|
|
1022
|
+
// The dialog can detach mid-flight (state replaced/closed elsewhere) - a
|
|
1023
|
+
// write onto a dead object would be silently lost, same race runFileMutation
|
|
1024
|
+
// guards below.
|
|
1025
|
+
if (state.files.dialog !== d) { announce('delete finished after the dialog closed'); render(); return; }
|
|
997
1026
|
const failed = targets.filter((_, i) => results[i].status === 'rejected');
|
|
998
1027
|
const okCount = targets.length - failed.length;
|
|
999
1028
|
if (!failed.length) {
|
|
1000
1029
|
state.files.dialog = null;
|
|
1030
|
+
restoreFileDialogFocus(d._trigger);
|
|
1001
1031
|
marked.clear(); state.files._lastMarkIdx = null;
|
|
1002
1032
|
announce('deleted ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries'));
|
|
1003
1033
|
// Patch the visible list immediately instead of waiting on a second full
|
|
@@ -1026,22 +1056,28 @@ async function runBulkMove(destDir) {
|
|
|
1026
1056
|
if (!d || d.kind !== 'bulk-move' || d.busy) return;
|
|
1027
1057
|
const marked = filesMarked();
|
|
1028
1058
|
const targets = (state.files.entries || []).filter((e) => marked.has(e.path || e.name));
|
|
1029
|
-
if (!targets.length) { state.files.dialog = null; render(); return; }
|
|
1059
|
+
if (!targets.length) { state.files.dialog = null; restoreFileDialogFocus(d._trigger); render(); return; }
|
|
1030
1060
|
d.busy = true; d.error = null; render();
|
|
1031
1061
|
// Validate the destination once up front so 20 identical 403s read as one
|
|
1032
1062
|
// clear message instead of a partial-failure split.
|
|
1033
1063
|
try { await B.statPath(state.backend, destDir); }
|
|
1034
1064
|
catch (e) {
|
|
1065
|
+
if (state.files.dialog !== d) { announce(fileMutationCopy(e)); render(); return; }
|
|
1035
1066
|
d.busy = false;
|
|
1036
1067
|
d.error = e.status === 404 ? 'that folder does not exist' : fileMutationCopy(e);
|
|
1037
1068
|
render(); return;
|
|
1038
1069
|
}
|
|
1039
1070
|
const results = await Promise.allSettled(
|
|
1040
1071
|
targets.map((e) => B.moveEntry(state.backend, e.path, destDir)));
|
|
1072
|
+
// The dialog can detach mid-flight (state replaced/closed elsewhere) - a
|
|
1073
|
+
// write onto a dead object would be silently lost, same race runFileMutation
|
|
1074
|
+
// guards below.
|
|
1075
|
+
if (state.files.dialog !== d) { announce('move finished after the dialog closed'); render(); return; }
|
|
1041
1076
|
const failed = targets.filter((_, i) => results[i].status === 'rejected');
|
|
1042
1077
|
const okCount = targets.length - failed.length;
|
|
1043
1078
|
if (!failed.length) {
|
|
1044
1079
|
state.files.dialog = null;
|
|
1080
|
+
restoreFileDialogFocus(d._trigger);
|
|
1045
1081
|
marked.clear(); state.files._lastMarkIdx = null;
|
|
1046
1082
|
announce('moved ' + okCount + ' ' + (okCount === 1 ? 'entry' : 'entries'));
|
|
1047
1083
|
// Moved entries leave the current dir - patch them out immediately rather
|
|
@@ -1065,7 +1101,9 @@ function closeFileDialog() {
|
|
|
1065
1101
|
// A mid-flight close would orphan the mutation's result and swallow its
|
|
1066
1102
|
// error - hold the dialog open until the operation settles.
|
|
1067
1103
|
if (state.files.dialog?.busy) { announce('still working - please wait'); return; }
|
|
1104
|
+
const trigger = state.files.dialog?._trigger;
|
|
1068
1105
|
state.files.dialog = null; render();
|
|
1106
|
+
restoreFileDialogFocus(trigger);
|
|
1069
1107
|
}
|
|
1070
1108
|
async function runFileMutation(fn, doneMsg, patch) {
|
|
1071
1109
|
const d = state.files.dialog;
|
|
@@ -1074,6 +1112,7 @@ async function runFileMutation(fn, doneMsg, patch) {
|
|
|
1074
1112
|
try {
|
|
1075
1113
|
await fn();
|
|
1076
1114
|
state.files.dialog = null;
|
|
1115
|
+
restoreFileDialogFocus(d._trigger);
|
|
1077
1116
|
announce(doneMsg);
|
|
1078
1117
|
if (patch) {
|
|
1079
1118
|
// Patch the visible list immediately, matching the bulk-delete/move
|
|
@@ -1131,6 +1170,32 @@ async function uploadFiles(fileList) {
|
|
|
1131
1170
|
if (!(state.files.uploads || []).some(i => i.error)) state.files.uploads = null;
|
|
1132
1171
|
render();
|
|
1133
1172
|
}
|
|
1173
|
+
// Pasted-image handler for the chat composer: upload each pasted file through
|
|
1174
|
+
// the same confined endpoint the Files tab uses (into the chat's current cwd),
|
|
1175
|
+
// then insert the resulting relative path into the draft. Falls back to an
|
|
1176
|
+
// announce() if there's no cwd to upload into (chat hasn't picked one yet).
|
|
1177
|
+
async function pastedFilesToChat(files) {
|
|
1178
|
+
const list = Array.from(files || []);
|
|
1179
|
+
if (!list.length) return;
|
|
1180
|
+
const dir = state.chatCwd;
|
|
1181
|
+
if (!dir) { announce('set a working directory before pasting files'); return; }
|
|
1182
|
+
const inserted = [];
|
|
1183
|
+
for (const f of list) {
|
|
1184
|
+
try {
|
|
1185
|
+
await B.uploadFile(state.backend, dir, f);
|
|
1186
|
+
inserted.push(f.name);
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
announce('paste upload failed: ' + fileMutationCopy(e));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (inserted.length) {
|
|
1192
|
+
const draft = state.chat.draft || '';
|
|
1193
|
+
state.chat.draft = draft + (draft && !draft.endsWith('\n') && !draft.endsWith(' ') ? ' ' : '') + inserted.join(' ');
|
|
1194
|
+
debouncedPersistDraft();
|
|
1195
|
+
announce('pasted ' + plural(inserted.length, 'file') + ' into ' + pathBasename(dir));
|
|
1196
|
+
render();
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1134
1199
|
// 'replace' on a 409 row: re-PUT the same file with overwrite=1.
|
|
1135
1200
|
async function retryUploadOverwrite(it) {
|
|
1136
1201
|
if (!it || it._retrying || !it._file) return;
|
|
@@ -1720,9 +1785,14 @@ function computeLiveSessions() {
|
|
|
1720
1785
|
lastTs: t.last,
|
|
1721
1786
|
errors,
|
|
1722
1787
|
currentTool: t.toolRunning ? (t.toolName || 'tool') : '',
|
|
1723
|
-
// Apply stale detection to external sessions too: no recent activity + no
|
|
1788
|
+
// Apply stale detection to external sessions too: no recent activity + no
|
|
1789
|
+
// running tool = stale. Same STALE_AFTER_MS as owned sessions (line 1691)
|
|
1790
|
+
// AND the inclusion filter above (line 1731) - a card must never report
|
|
1791
|
+
// 'stale' before it's even old enough to be dropped from the list, and an
|
|
1792
|
+
// owned vs external session in the identical objective state must read
|
|
1793
|
+
// the same status.
|
|
1724
1794
|
status: (lastErrorTs && (nowS - lastErrorTs) <= STALE_AFTER_MS) ? 'error'
|
|
1725
|
-
: (!t.toolRunning && t.last && (nowS - t.last) > STALE_AFTER_MS
|
|
1795
|
+
: (!t.toolRunning && t.last && (nowS - t.last) > STALE_AFTER_MS ? 'stale' : 'running'),
|
|
1726
1796
|
stopping: false,
|
|
1727
1797
|
});
|
|
1728
1798
|
}
|
|
@@ -2004,6 +2074,11 @@ function chatMain() {
|
|
|
2004
2074
|
children: [
|
|
2005
2075
|
h('span', { key: 'petxt' }, 'New turns stay in this tab but will not survive a reload - export the transcript from settings. '),
|
|
2006
2076
|
Btn({ key: 'pedismiss', onClick: () => { state.chat.persistError = false; render(); }, children: 'dismiss' })] }));
|
|
2077
|
+
} else if (state.chat.persistTruncated) {
|
|
2078
|
+
banners.push(Alert({ key: 'ptrunc', kind: 'warn', title: 'Older turns will not survive a reload',
|
|
2079
|
+
children: [
|
|
2080
|
+
h('span', { key: 'ptrunctxt' }, 'This transcript is longer than the ' + PERSIST_MSG_CAP + ' turns saved locally - a reload keeps only the most recent ' + PERSIST_MSG_CAP + '. Export the full transcript from settings first. '),
|
|
2081
|
+
Btn({ key: 'ptruncdismiss', onClick: () => { state.chat.persistTruncated = false; render(); }, children: 'dismiss' })] }));
|
|
2007
2082
|
}
|
|
2008
2083
|
if (state.agentsError) {
|
|
2009
2084
|
banners.push(Alert({ key: 'agerr', kind: 'error', title: 'Could not load agents from the server',
|
|
@@ -2126,9 +2201,23 @@ function chatMain() {
|
|
|
2126
2201
|
cwdDraft: state.cwdDraft,
|
|
2127
2202
|
cwdError: state.cwdError || null,
|
|
2128
2203
|
cwdChecking: !!state.cwdChecking,
|
|
2129
|
-
//
|
|
2130
|
-
//
|
|
2131
|
-
|
|
2204
|
+
// Pasted images upload through the same confined endpoint the Files tab
|
|
2205
|
+
// uses, into the chat's current cwd, then insert the resulting relative
|
|
2206
|
+
// path into the draft - matching the desktop-composer expectation
|
|
2207
|
+
// instead of the prior permanent no-op.
|
|
2208
|
+
onPasteFiles: (files) => pastedFilesToChat(files),
|
|
2209
|
+
// Dropped files resolve their server-relative path via the confined
|
|
2210
|
+
// Files API and insert it into the draft, matching desktop-composer
|
|
2211
|
+
// expectations instead of the prior dead-end 'not supported' toast.
|
|
2212
|
+
onDropFiles: (files) => pastedFilesToChat(files),
|
|
2213
|
+
// The toolbar emoji button opens the SAME inline picker typing ':' does
|
|
2214
|
+
// (EMOJI_TRIGGER_RE) - append the trigger character rather than
|
|
2215
|
+
// duplicating the picker-open logic, which lives inside the composer.
|
|
2216
|
+
onEmoji: () => {
|
|
2217
|
+
const draft = state.chat.draft || '';
|
|
2218
|
+
state.chat.draft = draft + (draft && !draft.endsWith(' ') ? ' ' : '') + ':';
|
|
2219
|
+
render();
|
|
2220
|
+
},
|
|
2132
2221
|
// Long threads render a capped window with a 'show earlier' row (the
|
|
2133
2222
|
// chat-side equivalent of history's eventsLimit), reset per conversation.
|
|
2134
2223
|
shownMessages: state.chat.shownMessages || undefined,
|
|
@@ -2270,8 +2359,16 @@ function trimPartForStorage(p) {
|
|
|
2270
2359
|
}
|
|
2271
2360
|
function persistChat() {
|
|
2272
2361
|
try {
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2362
|
+
const eligible = state.chat.messages.filter(m => !isEmptyTurn(m));
|
|
2363
|
+
// On-screen transcript can exceed the cap; a reload only restores the last
|
|
2364
|
+
// PERSIST_MSG_CAP turns - surface that once so it's not a silent surprise,
|
|
2365
|
+
// same Alert pattern as the sibling quota-failure banner below.
|
|
2366
|
+
const wasCapped = eligible.length > PERSIST_MSG_CAP;
|
|
2367
|
+
if (wasCapped !== !!state.chat.persistTruncated) {
|
|
2368
|
+
state.chat.persistTruncated = wasCapped;
|
|
2369
|
+
scheduleRender();
|
|
2370
|
+
}
|
|
2371
|
+
const msgs = eligible
|
|
2275
2372
|
.slice(-PERSIST_MSG_CAP)
|
|
2276
2373
|
.map(m => ({ id: m.id, role: m.role, content: m.content, time: m.time, costUsd: m.costUsd, stopped: m.stopped || undefined, parts: Array.isArray(m.parts) ? m.parts.map(trimPartForStorage) : m.parts }));
|
|
2277
2374
|
const draft = (state.chat.draft || '').trim() ? state.chat.draft : '';
|
|
@@ -2704,12 +2801,12 @@ function historyMain() {
|
|
|
2704
2801
|
const sess = (Array.isArray(state.sessions) ? state.sessions : []).find(s => s.sid === state.selectedSid);
|
|
2705
2802
|
const lede = sess
|
|
2706
2803
|
? (projectLabel(sess.project) || sess.cwd || 'unknown location') + ' · ' + plural(sess.events || 0, 'event') + ' · ' + plural(sess.userTurns || 0, 'turn') + ' · ' + fmtRelTime(sess.last)
|
|
2707
|
-
:
|
|
2804
|
+
: UNTITLED_CONVERSATION;
|
|
2708
2805
|
|
|
2709
2806
|
const head = PageHeader({
|
|
2710
2807
|
compact: true,
|
|
2711
2808
|
dense: true,
|
|
2712
|
-
title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || sess?.cwd ||
|
|
2809
|
+
title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || sess?.cwd || UNTITLED_CONVERSATION, 40, 80),
|
|
2713
2810
|
lede,
|
|
2714
2811
|
});
|
|
2715
2812
|
|
|
@@ -2977,7 +3074,7 @@ function resumeSidLabel(sid) {
|
|
|
2977
3074
|
const arr = Array.isArray(state.sessions) ? state.sessions : [];
|
|
2978
3075
|
const sess = arr.find((s) => s.sid === sid);
|
|
2979
3076
|
const label = sess && (projectLabel(sess.title) || projectLabel(sess.project));
|
|
2980
|
-
return label ||
|
|
3077
|
+
return label || UNTITLED_CONVERSATION;
|
|
2981
3078
|
}
|
|
2982
3079
|
|
|
2983
3080
|
function uniqueProjects() {
|
|
@@ -2995,7 +3092,21 @@ function uniqueProjects() {
|
|
|
2995
3092
|
// stays visible and stoppable from anywhere (active polling is global).
|
|
2996
3093
|
function runningPanel() {
|
|
2997
3094
|
const running = Array.isArray(state.active) ? state.active : [];
|
|
2998
|
-
if (!running.length)
|
|
3095
|
+
if (!running.length) {
|
|
3096
|
+
// A send is in flight but the active-session poll hasn't caught up yet
|
|
3097
|
+
// (up to ~3s) - without this, the Live tab's existence is undiscoverable
|
|
3098
|
+
// during that gap even though the chat is, in fact, already running.
|
|
3099
|
+
if (state.chat.busy) {
|
|
3100
|
+
return Panel({
|
|
3101
|
+
key: 'runningPanelPending',
|
|
3102
|
+
title: 'running',
|
|
3103
|
+
children: [h('div', { key: 'runpending', class: 'resume-banner', role: 'group' },
|
|
3104
|
+
h('span', { key: 'runpendinglbl', class: 't-meta' }, 'this chat will also appear in live once it starts'),
|
|
3105
|
+
Btn({ key: 'runpendinglive', onClick: () => navTo('live'), children: 'view live' }))],
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
2999
3110
|
const stopping = state.live.stopping || new Set();
|
|
3000
3111
|
return Panel({
|
|
3001
3112
|
key: 'runningPanel',
|
|
@@ -3108,15 +3219,16 @@ function healthSummary() {
|
|
|
3108
3219
|
if (hh.version) bits.push(['v' + hh.version, 'Server version']);
|
|
3109
3220
|
if (typeof hh.agents === 'number') bits.push([hh.agents + ' agents', 'Agents registered on the server']);
|
|
3110
3221
|
if (typeof hh.activeExecutions === 'number') bits.push([hh.activeExecutions + ' active', 'Chats currently executing']);
|
|
3111
|
-
// db chip uses the
|
|
3112
|
-
// (not ok/down,
|
|
3113
|
-
//
|
|
3114
|
-
//
|
|
3222
|
+
// db chip uses the SAME connected/offline register as the connection chip
|
|
3223
|
+
// above (not ok/down, and not a third online/offline register) so the same
|
|
3224
|
+
// up/down category of fact reads the same word in both chips of this row.
|
|
3225
|
+
// Always render it: when /health is partial and hh.db is absent, show
|
|
3226
|
+
// 'db unknown' rather than dropping the chip and leaving the db state ambiguous.
|
|
3115
3227
|
// Only show 'db unknown' when health has been fetched (not on initial null/unknown state).
|
|
3116
3228
|
if (hh.status && hh.status !== 'unknown') {
|
|
3117
|
-
bits.push(['db ' + (hh.db ? (hh.db.ok ? '
|
|
3229
|
+
bits.push(['db ' + (hh.db ? (hh.db.ok ? 'connected' : 'offline') : 'unknown'), 'History database status']);
|
|
3118
3230
|
} else if (hh.db) {
|
|
3119
|
-
bits.push(['db ' + (hh.db.ok ? '
|
|
3231
|
+
bits.push(['db ' + (hh.db.ok ? 'connected' : 'offline'), 'History database status']);
|
|
3120
3232
|
}
|
|
3121
3233
|
return h('div', { key: 'hp', class: 'health-summary' + (ok ? ' health-ok' : ''), role: 'group', 'aria-label': 'Backend health' },
|
|
3122
3234
|
...bits.map(([b, t], i) => h('span', { key: 'hb' + i, class: 'health-chip', title: t }, b)));
|