agentgui 1.0.1042 → 1.0.1044
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-38b7e24dbcba2f3e-639.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-cdf04ff6a6a0aa6f-671.md +8 -0
- package/.gm/memories/mem-e490a181de6469fc-487.md +8 -0
- package/.gm/memories/mem-fe3faa1652f07239-480.md +8 -0
- package/AGENTS.md +7 -9
- 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-38b7e24dbcba2f3e-639
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783844902974
|
|
5
|
+
updated: 1783844902974
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
HISTORICAL/SUPERSEDED (do not trust): agentgui briefly (2026-05-29) loaded anentrypoint-design from unpkg@latest at runtime instead of a local vendored copy, for always-latest tracking at the cost of requiring network + drift on upstream publish. This was REVERTED - as of the 2026-06-04/06-05 runs, index.html loads the DS kit from the LOCAL ./vendor/anentrypoint-design/247420.{js,css} again (re-confirmed live via browser witness on the 37th run, 2026-07-12: <link href='./vendor/anentrypoint-design/247420.css'>). AGENTS.md's 'DS load = LOCAL vendor' section is the current truth; any memory claiming unpkg is the live source is stale.
|
|
@@ -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-cdf04ff6a6a0aa6f-671
|
|
3
|
+
ns: default
|
|
4
|
+
created: 1783844901564
|
|
5
|
+
updated: 1783844901564
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
agentgui webjsx applyDiff array-keying rule: children arrays that mix keyed VElements with raw text/numbers crash in vendor/webjsx/applyDiff.js with 'Cannot read properties of undefined (reading key)'. webjsx requires ALL siblings in an array prop (children, right, left, breadcrumbs, etc.) to either ALL be VElements with keys or ALL be non-keyed - passing Crumb({right:[h('span',{key:'a'},'A'), h('span',{key:'b'},'B'), 'connected']}) crashes because the render loop tries to read .key on the bare string. Fix: wrap any bare strings as keyed VElements, e.g. h('span',{key:'dot'},'connected'). Surfaced 2026-05-04 in crumbRight construction in site/app/js/app.js view().
|
|
@@ -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-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 logic+predictability sweep (2026-07-12) — thirty-seventh run
|
|
4
|
+
|
|
5
|
+
`gui-logic-predictability` workflow wf_5eec2403-09d (30 agents, 6 hunt lenses + verify + synthesize, 0 errors) — 15 confirmed findings, all implemented. App (`site/app/js/app.js`): `stopActiveChat` no longer leaves the per-card stop button permanently stuck on a failed cancel (deletes the sid from the stopping Set + surfaces an error on rejection instead of swallowing it); Files dialogs (rename/delete/mkdir/bulk) now restore keyboard focus to the triggering element on close, mirroring the existing cwd-edit pattern; `runBulkDelete`/`runBulkMove` gained the same stale-dialog guard `runFileMutation` already had; the "Untitled conversation" fallback consolidated to one `UNTITLED_CONVERSATION` constant (3 call sites had drifted casing); the settings db health chip now reads `connected`/`offline` instead of a third `online`/`offline` register, matching the adjacent connection chip; history search placeholder/caption now discloses it searches all projects' titles+messages; `runningPanel()` shows a lightweight "will also appear in live" hint during the send-to-active-poll gap instead of returning null; `computeLiveSessions()`'s external-session stale threshold no longer silently multiplies `STALE_AFTER_MS` by an undocumented 0.6 (owned and external sessions in the same objective state now report the same status); pasted/dropped composer files upload through the existing confined `PUT /api/upload-file` and insert the resulting path into the draft (was a permanent no-op/dead-end toast); the sessions rail's 60-item cap gained a load-more affordance (`state.sessionsLimit += 60`), mirroring the existing `eventsLimit` "load older" pattern; `PERSIST_MSG_CAP` truncation now surfaces a dismissible banner instead of silently dropping older turns from localStorage. Kit (`design/src/components/{chat,agent-chat,content,files,sessions}.js`, `app-shell.css`, `chat.css`): deleted dead `onAttach`/`onMenu` composer toolbar plumbing (no such feature existed); `AgentChat` now forwards `onEmoji` to its internal `ChatComposer` call (the emoji toolbar button was invisible because `AgentChat` — the wrapper agentgui actually calls, not `ChatComposer` directly — never destructured the prop; caught mid-EXECUTE via a `transition to=PLAN` re-scope); `SearchInput` and the Files filter input both gained Escape-to-clear + a visually-hidden `aria-live` result-count region; `ConversationList` gained `hasMore`/`onLoadMore`; composer textarea `overflow-y` changed from focus-only `auto` to always-`auto` so a capped-height composer keeps its scroll cue after blur. Kit built+tested (`bun test.js` all pass, all build lints pass), re-vendored, browser-witnessed live on buildesk across chat/history/settings/files (0 blocking errors on clean navigations). Kit pushed `af028ff`, agentgui pushed `a3b844d`, all CI green both repos. **Standing gotcha reconfirmed: this env's `gm browser` verb text-protocol (`dom=<selector>\n<script>`) only evaluates the selector query, not the trailing script — use `dom=<precise-selector>` alone to inspect attributes/rect, not as a general page-eval channel. Also: embedding Basic-Auth creds directly in a `page.goto` URL (`https://user:pass@host/...`) breaks the app's own same-origin `fetch()` calls with "Request cannot be constructed from a URL that includes credentials" — do one embedded-creds navigation to seed the browser's per-origin Basic-Auth cache, then navigate again WITHOUT embedded creds (plain URL or `?token=`) for any witness that needs real fetches to succeed.**
|
|
6
|
+
|
|
3
7
|
## GUI AI-tell sweep (2026-07-10) — thirty-sixth run
|
|
4
8
|
|
|
5
9
|
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.**
|
|
@@ -226,21 +230,15 @@ Node v24 lacks a prebuilt better-sqlite3 binary; the node path needs a from-sour
|
|
|
226
230
|
|
|
227
231
|
## webjsx applyDiff Array Keying (2026-05-04)
|
|
228
232
|
|
|
229
|
-
**
|
|
230
|
-
|
|
231
|
-
`webjsx` requires all siblings in an array prop (children, right, left, breadcrumbs, etc.) to either ALL be VElements with keys or ALL be non-keyed. Passing `Crumb({ right: [h('span', { key: 'a' }, 'A'), h('span', { key: 'b' }, 'B'), 'connected'] })` crashes on render with `TypeError: Cannot read properties of undefined (reading 'key')` at line 247420.js when the render loop tries to read `.key` on the bare string.
|
|
232
|
-
|
|
233
|
-
Fix: wrap any bare strings as keyed VElements: `h('span', { key: 'dot' }, 'connected')`. Rule: if ANY sibling in an array has a key, ALL siblings must be VElements (no raw strings or numbers).
|
|
234
|
-
|
|
235
|
-
Surfaced 2026-05-04 while validating the chat surface in `site/app/`. Fix applied to crumbRight construction in `site/app/js/app.js` view().
|
|
233
|
+
**Rule still binding: mixing keyed VElements with raw strings/numbers in any webjsx array prop crashes `applyDiff`.** Full detail in rs-learn (recall "agentgui webjsx applyDiff array-keying rule").
|
|
236
234
|
|
|
237
235
|
## Live History Stream Wiring (2026-05-05)
|
|
238
236
|
|
|
239
237
|
`site/app/js/app.js` opens an SSE EventSource on `navTo('history')` (`B.streamHistory`, `state.live` shape, hello/event/conversation/error dispatch, rAF-throttled renders) against ccsniff's in-process `/v1/history/stream`; first `/v1/history/*` request triggers a 30-90s synchronous JSONL walk (`CLAUDE_PROJECTS_DIR`) — health-check timeouts during that window are normal. Full detail in rs-learn (recall "agentgui Live History Stream Wiring").
|
|
240
238
|
|
|
241
|
-
## Runtime CDN: GUI loaded from unpkg
|
|
239
|
+
## Runtime CDN: GUI loaded from unpkg — SUPERSEDED, do not trust
|
|
242
240
|
|
|
243
|
-
|
|
241
|
+
This 2026-05-29 unpkg-at-runtime approach was reverted; see "DS load = LOCAL vendor" below for the current truth (re-confirmed live on the 37th run, 2026-07-12). Historical detail in rs-learn (recall "agentgui unpkg superseded local vendor").
|
|
244
242
|
|
|
245
243
|
## Agent/model/session management (2026-05-28)
|
|
246
244
|
|
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)));
|