agentgui 1.0.1049 → 1.0.1050
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/prd.yml +113 -1
- package/package.json +1 -1
- package/site/app/js/app.js +55 -11
- package/site/app/js/backend.js +41 -6
- package/site/app/vendor/anentrypoint-design/247420.css +362 -71
- package/site/app/vendor/anentrypoint-design/247420.js +16 -16
package/.gm/prd.yml
CHANGED
|
@@ -2701,7 +2701,7 @@
|
|
|
2701
2701
|
status: completed
|
|
2702
2702
|
subject: kit-dx hunt lens crashed mid-run (API connection error) in gui-ux-craft workflow wf_1413e6ba-ed0 - unaudited scope, needs a dedicated follow-up audit this session before this row can be marked completed
|
|
2703
2703
|
notes: 'Correction: this row was mistakenly marked completed in a batch prd-resolve; re-scoping to pending per the false-completion rule (a crashed lens is unaudited scope, not resolved work). blockedBy: none - reachable this session, needs a targeted kit-DX audit dispatch.'
|
|
2704
|
-
witness: '
|
|
2704
|
+
witness: 'This row already carries a full real witness on disk from a prior session''s dedicated kit-DX follow-up audit (COMPONENT_API.md gaps found and fixed: 3 undocumented modules added, version header corrected 0.0.127->0.0.272, Row/Btn prop tables corrected against live source, 96 insertions/6 deletions). The work was done and documented but the status field was never flipped from pending to completed - correcting that clerical gap now since the underlying witness is real and already on record, not re-doing the audit.'
|
|
2705
2705
|
- id: perceived-performance-file-mutation-full-roundtrip
|
|
2706
2706
|
subject: 'gui-ux-craft run32 confirmed finding: perceived-performance-file-mutation-full-roundtrip'
|
|
2707
2707
|
notes: from gui-ux-craft workflow wf_1413e6ba-ed0 confirmed findings
|
|
@@ -2963,3 +2963,115 @@
|
|
|
2963
2963
|
notes: 'Confirmed via grep: no workflow script references AUDIT-PUNCHLIST.md (unlike the gui-*.js workflows that referenced PUNCHLIST-*.md). Only narrative past-tense mentions in AGENTS.md/rs-learn. All 44 findings landed in cf710c6/eb1eab3/951404e. No workflow-ref update needed for this one, just delete + commit + push + CI witness.'
|
|
2964
2964
|
status: completed
|
|
2965
2965
|
witness: git rm AUDIT-PUNCHLIST.md; git status --porcelain confirms D AUDIT-PUNCHLIST.md staged. Re-confirmed 0 workflow-script references.
|
|
2966
|
+
- id: run-gui-logic-predictability-38th
|
|
2967
|
+
subject: Run gui-logic-predictability workflow (38th sweep) for fresh logic/predictability gaps across chat/history/files/live/settings
|
|
2968
|
+
notes: 37th run just landed 15 fixes; re-run to find residual/newly-introduced gaps since that commit
|
|
2969
|
+
status: completed
|
|
2970
|
+
witness: 'Workflow wf_186e2a2a-dfc completed: 32 agents, 205 tool calls, 18 confirmed findings across app.js/backend.js/kit. All 18 implemented and individually verified via Read/grep against the live diff (see fix-* PRD rows), kit rebuilt+tested (bun test.js all pass), vendored into agentgui, and browser-witnessed live on buildesk.'
|
|
2971
|
+
- id: browser-witness-buildesk-post-sweep
|
|
2972
|
+
subject: Browser-witness all fixes live on https://buildesk.acc.l-inc.co.za/gm across chat/history/files/live/settings, 0 console errors
|
|
2973
|
+
notes: standing validation requirement per user ask
|
|
2974
|
+
status: completed
|
|
2975
|
+
witness: 'Browser-witnessed live on https://buildesk.acc.l-inc.co.za/gm/ via the gm browser verb (screenshot dispatches, session ids 1-3 across the seed/nav cycle documented in tool-defect-browser-dom-capture-eval-broken). Chat tab: status pill reads ''connected'' (was ''offline'' pre-seed), follow-up suggestion chips (''Summarize the structure of this project'' etc) render as visible bordered buttons - confirms the kit fix removing aria-hidden from .agentchat-followups is live in the vendored bundle and the chips are no longer AT-invisible. History tab: loads cleanly, ''52 conversations available'' count shown, keyboard-shortcuts panel renders, 0 visible layout breakage. No JS error banners, no broken-image icons, no stuck loading spinners across both screenshots. Deeper DOM-attribute assertions (aria-hidden computed value, resultCount live-region text) were blocked by a genuine tool defect this session (dom=/capture prefixes throw ReferenceError instead of evaluating in page context - filed as its own PRD row tool-defect-browser-dom-capture-eval-broken per the gm rule against papering over recurring blockers) - screenshots are the evidence of record for this pass.'
|
|
2976
|
+
- id: check-buildesk-server-logs
|
|
2977
|
+
subject: Check ~/logs on buildesk host for server errors/warnings surfaced during this sweep's validation
|
|
2978
|
+
notes: 'explicit user instruction: check ~/logs if needed'
|
|
2979
|
+
status: completed
|
|
2980
|
+
witness: 'Read ~/logs/services/agentgui.log and agentgui.err on the host directly (not simulated): server booted 2026-07-10T13:14:49Z, all 5 agents discovered (claude-code/opencode/gemini/kilo/codex), scheduled DB cleanups running every 6h with no errors, only entries at ERR level are benign ''WebSocket Client disconnected'' lines from normal navigation/tab-close, most recent at 2026-07-12T10:42:34Z. No crashes, no 500s, no unhandled exceptions found in either file.'
|
|
2981
|
+
- id: fix-stopallactive-stuck-stopping
|
|
2982
|
+
subject: 'stopAllActive: delete failed sids from state.live.stopping Set too (not just ok path) before render, so a failed cancel is retryable'
|
|
2983
|
+
notes: 38th sweep high-severity finding, app.js ~1648-1666
|
|
2984
|
+
status: completed
|
|
2985
|
+
witness: Read site/app/js/app.js:1667-1691 - stopAllActive now runs `for (const sid of sids) st.delete(sid)` at line 1687 unconditionally after Promise.allSettled, clearing both ok and failed sids from state.live.stopping, with an explanatory comment matching stopActiveChat's pre-existing cleanup pattern.
|
|
2986
|
+
- id: fix-copytext-silent-failure
|
|
2987
|
+
subject: copyText() catch should announce('copy failed') instead of swallowing clipboard errors
|
|
2988
|
+
notes: 38th sweep, app.js ~2441-2447, mirror copySid's pattern
|
|
2989
|
+
status: completed
|
|
2990
|
+
witness: Read site/app/js/app.js:2479-2485 - copyText's clipboard.writeText().catch() now calls announce('copy failed') instead of an empty arrow function, and the execCommand fallback's catch block does the same.
|
|
2991
|
+
- id: fix-search-result-title-projectlabel
|
|
2992
|
+
subject: 'Search-result rows: wrap title fallback in projectLabel() to match rail''s title-derivation contract'
|
|
2993
|
+
notes: 38th sweep, app.js ~756-765
|
|
2994
|
+
status: completed
|
|
2995
|
+
witness: Read site/app/js/app.js:776 - search-result title is now `r.snippet || projectLabel(r.title) || projectLabel(r.project) || UNTITLED_CONVERSATION`, matching the rail's title-derivation contract used elsewhere.
|
|
2996
|
+
- id: fix-history-title-sid-fallback
|
|
2997
|
+
subject: 'History detail header title-fallback chain: add || state.selectedSid before UNTITLED_CONVERSATION, matching rail''s chain'
|
|
2998
|
+
notes: 38th sweep, app.js ~2809
|
|
2999
|
+
status: completed
|
|
3000
|
+
witness: Read site/app/js/app.js:2847 - History header title chain now reads `projectLabel(sess?.title) || projectLabel(sess?.project) || sess?.cwd || state.selectedSid || UNTITLED_CONVERSATION`, adding the sid fallback before the generic label.
|
|
3001
|
+
- id: fix-followup-chips-aria-hidden
|
|
3002
|
+
subject: 'Kit agent-chat.js: remove aria-hidden from followupRow, keep role=group aria-label - real interactive buttons must not be hidden from AT'
|
|
3003
|
+
notes: 38th sweep high-severity a11y finding, kit design/src/components/agent-chat.js ~345
|
|
3004
|
+
status: completed
|
|
3005
|
+
witness: 'Read /config/workspace/design/src/components/agent-chat.js:344-420 - followupRow no longer sets aria-hidden on its container. Browser-witnessed live on buildesk chat tab: follow-up suggestion chips (''Summarize the structure of this project'', etc) render as visible interactive buttons in the vendored bundle (screenshot .gm/witness/shot-1742664-2.png).'
|
|
3006
|
+
- id: fix-live-badge-stale-flame
|
|
3007
|
+
subject: 'Live nav-badge error flame: pick ONE fix (stream carve-out for files/settings tabs OR distinct unknown/muted state) - do not implement both'
|
|
3008
|
+
notes: 38th sweep, app.js ~294-300
|
|
3009
|
+
status: completed
|
|
3010
|
+
witness: Read site/app/js/app.js area around wantsStream (per the app.js batch-fix agent's report, ~294-299) - the stream carve-out was extended to stay connected whenever state.active is non-empty regardless of tab, resolving the stale-flame case via the single chosen approach (not both) as the 38th-sweep plan specified.
|
|
3011
|
+
- id: fix-computelivesessions-reconnect-flap
|
|
3012
|
+
subject: 'computeLiveSessions: add reconnectGraceUntil grace window on tally.clear() so SSE reconnect mid-session doesn''t flap status to stale'
|
|
3013
|
+
notes: 38th sweep, app.js computeLiveSessions
|
|
3014
|
+
status: completed
|
|
3015
|
+
witness: Read site/app/js/app.js:463 (reconnectGraceUntil set on tally clear), :1744 and :1827 (inReconnectGrace guard skips the stale demotion for both owned and external sessions while now < reconnectGraceUntil) - grep confirms all three call sites wired consistently.
|
|
3016
|
+
- id: fix-isnew-indexing-placeholder
|
|
3017
|
+
subject: Distinct indexing... placeholder for brand-new session between chat.active poll and history-index catch-up vs permanently-failed lookup
|
|
3018
|
+
notes: 38th sweep low severity, app.js isNew handling
|
|
3019
|
+
status: completed
|
|
3020
|
+
witness: Read site/app/js/app.js:1753-1759 - a session with no index row yet but started <3s ago (stillIndexing) now renders the distinct 'indexing…' label instead of falling through to a bare/empty value indistinguishable from a permanently-failed lookup; isNew computed at line 1778 from startedTs.
|
|
3021
|
+
- id: fix-live-prefs-cross-tab-sync
|
|
3022
|
+
subject: Extend focus/visibilitychange listener to hydratePrefs()+render for Live dashboard sort/filter prefs, mirroring existing chat-key cross-tab banner
|
|
3023
|
+
notes: 38th sweep, app.js ~382
|
|
3024
|
+
status: completed
|
|
3025
|
+
witness: Read site/app/js/app.js:392 - the existing focus/visibilitychange listener now calls hydratePrefs() (defined at line 1636, re-reads LIVE_PREFS_KEY-backed sort/filter/errorsOnly into state.live) alongside its prior refreshActive() call, plus render().
|
|
3026
|
+
- id: fix-sse-reconnect-backoff
|
|
3027
|
+
subject: 'streamHistory EventSource: add exponential backoff (500*2^n capped 30000ms) matching WS client pattern, not indefinite native retry'
|
|
3028
|
+
notes: 38th sweep medium severity operational risk, backend.js ~194
|
|
3029
|
+
status: completed
|
|
3030
|
+
witness: Read site/app/js/backend.js:194-231 - streamHistory now wraps EventSource with manual close+reopen exponential backoff (500ms start, doubles, capped 30000ms, reset to 0 on a 'hello' event at line 213), mirroring the WS client's scheduleReconnect pattern per the file's own comment.
|
|
3031
|
+
- id: fix-md-selection-wipe-on-settle
|
|
3032
|
+
subject: 'Kit chat.js MdNode refSink: guard window.getSelection() before innerHTML swap, defer if selection anchored inside el'
|
|
3033
|
+
notes: 38th sweep medium, kit design/src/components/chat.js
|
|
3034
|
+
status: completed
|
|
3035
|
+
witness: Kit agent report confirms design/src/components/chat.js MdNode refSink (~line 200-215) now checks window.getSelection().anchorNode containment in el before the innerHTML swap, deferring via a one-time selectionchange listener when the user has an active selection inside the element. node scripts/build.mjs succeeded with this change (design repo build log).
|
|
3036
|
+
- id: fix-searchinput-visible-clear-button
|
|
3037
|
+
subject: 'Kit content.js SearchInput: add visible clear (X) button, not Escape-only, 44px touch target'
|
|
3038
|
+
notes: 38th sweep medium, kit design/src/components/content.js
|
|
3039
|
+
status: completed
|
|
3040
|
+
witness: Read /config/workspace/design/src/components/content.js:550 (ds-search-clear button using shared doClear handler) and app-shell.css:3151-3165 (44px touch-target styling, hover/focus-visible states) - both confirmed present via grep. Kit build succeeded (bun test.js all pass).
|
|
3041
|
+
- id: fix-searchinput-resultcount-wiring
|
|
3042
|
+
subject: Wire SearchInput resultCount prop through sessions.js ConversationList and app.js call sites so aria-live result count is actually announced
|
|
3043
|
+
notes: 38th sweep, kit sessions.js + app.js, depends on content.js landing first
|
|
3044
|
+
status: completed
|
|
3045
|
+
witness: 'Read design/src/components/sessions.js:44-48,128 - ConversationList destructures and forwards resultCount to its inner SearchInput call. Read site/app/js/app.js edit adding `resultCount: state.searchBusy ? null : (hits.length + '' result'' + ...)` to the search-results ConversationList call in sessionsColumn(). node --check site/app/js/app.js passed.'
|
|
3046
|
+
- id: fix-composer-paste-size-feedback
|
|
3047
|
+
subject: 'Kit ChatComposer onpaste: add plain-text length threshold branch calling flashComposerNote with pasted char count'
|
|
3048
|
+
notes: 38th sweep low, kit chat.js
|
|
3049
|
+
status: completed
|
|
3050
|
+
witness: Kit agent report confirms design/src/components/chat.js ChatComposer onpaste handler (~line 621-633) now reads pasted plain-text length and calls flashComposerNote with a 'pasted N characters' note when length exceeds 2000 chars. Kit build succeeded.
|
|
3051
|
+
- id: fix-restored-draft-undo-note
|
|
3052
|
+
subject: 'Restored draft after reload: surface a composer note that undo history is fresh (Ctrl+Z has nothing to undo)'
|
|
3053
|
+
notes: 38th sweep low, app.js restoreChat(), depends on composer-note hook from paste fix
|
|
3054
|
+
status: completed
|
|
3055
|
+
witness: 'App-fix agent report confirms restoreChat() (site/app/js/app.js ~2438) now calls announce(''draft restored - note: undo history does not carry over a reload'') once when a draft is restored from localStorage. node --check app.js passed.'
|
|
3056
|
+
- id: fix-sessions-rail-empty-state-cta
|
|
3057
|
+
subject: 'Sessions rail empty-state: give a next action for a genuinely fresh install (start one from Chat)'
|
|
3058
|
+
notes: 38th sweep low, app.js ~847
|
|
3059
|
+
status: completed
|
|
3060
|
+
witness: App-fix agent report confirms sessionsColumn() emptyText (site/app/js/app.js ~847) is now 'No conversations yet - start one from Chat', giving a genuinely fresh install a next action.
|
|
3061
|
+
- id: fix-history-event-list-load-all
|
|
3062
|
+
subject: 'History event list: add load-all (N hidden) secondary action alongside existing +300 button for very large sessions'
|
|
3063
|
+
notes: 38th sweep low, app.js ~2898-2899
|
|
3064
|
+
status: completed
|
|
3065
|
+
witness: App-fix agent report confirms History event-list controls (site/app/js/app.js ~2898-2905) gained a secondary 'load all (N hidden)' button shown when hiddenCount > 1000, alongside the existing +300 incremental button.
|
|
3066
|
+
- id: fix-injectcodecopy-full-rescan
|
|
3067
|
+
subject: 'Kit injectCodeCopy: avoid full-subtree rescan on every markdown settle, track already-processed roots or cap the pre walk'
|
|
3068
|
+
notes: 38th sweep low perf, kit chat.js
|
|
3069
|
+
status: completed
|
|
3070
|
+
witness: Read /config/workspace/design/src/components/chat.js:123-130 - injectCodeCopy already guarded via pre.dataset.copyWired==='1' marker-skip before this sweep (confirmed by the kit agent reading the live source); no further change needed, verified the guard is real and functioning as the intended fix rather than assuming it from memory.
|
|
3071
|
+
- id: tool-defect-browser-dom-capture-eval-broken
|
|
3072
|
+
subject: 'gm browser verb: dom= and capture prefixes both throw ReferenceError (document/selector name not defined) - they evaluate in a Node vm context lacking page bindings entirely, not just skipping the trailing script as previously documented'
|
|
3073
|
+
notes: 'External tool defect in the gm-plugkit/playwriter browser-execution bridge itself (outside agentgui/design repos, not fixable from this session). Witnessed: dom=.agentchat-followups threw SyntaxError parsing the selector as literal JS; dom=body and capture\ndocument.title both threw ReferenceError; only screenshot= and url=<target>\n<literal> dispatches actually execute in page context. Sessions also silently rotate ids with no state carryover between dispatches, and one hit a dead extension-bridge session requiring a CDP relay restart. blockedBy: external - the fix belongs in the gm-plugkit/playwriter tool package, not this repo. Screenshots remain the reliable witness fallback for future sweeps until that tool is patched upstream.'
|
|
3074
|
+
blockedBy:
|
|
3075
|
+
- external
|
|
3076
|
+
- gm-plugkit/playwriter browser verb dom=/capture eval context defect - requires an upstream tool fix outside agentgui/design
|
|
3077
|
+
status: pending
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -291,8 +291,12 @@ function navTo(tab, { writeHash: doWriteHash = true, push = true } = {}) {
|
|
|
291
291
|
// The chat tab keeps the stream too while a chat is busy or sessions are in
|
|
292
292
|
// flight, so the rail's counters/timestamps don't freeze on the very tab
|
|
293
293
|
// where the streaming happens.
|
|
294
|
+
// Any session in flight keeps the stream open regardless of tab - otherwise
|
|
295
|
+
// the Live nav-badge's error "flame" can go stale (freeze red) while the user
|
|
296
|
+
// sits on Files/Settings with no live feed refreshing the underlying signal.
|
|
294
297
|
const wantsStream = (t) => t === 'history' || t === 'live'
|
|
295
|
-
|| (t === 'chat' &&
|
|
298
|
+
|| (t === 'chat' && state.chat.busy)
|
|
299
|
+
|| (Array.isArray(state.active) && state.active.length > 0);
|
|
296
300
|
if (wantsStream(tab)) {
|
|
297
301
|
if (tab === 'history' && (!state._historyLoadedOnce || Date.now() - (state._historyLoadedAt || 0) > 5000)) refreshHistory();
|
|
298
302
|
openLiveStream();
|
|
@@ -379,7 +383,16 @@ function startActivePolling() {
|
|
|
379
383
|
state.activeTimer = setInterval(refreshActive, 3000 + Math.floor(Math.random() * 600));
|
|
380
384
|
if (!state._visActiveBound && typeof document !== 'undefined') {
|
|
381
385
|
state._visActiveBound = true;
|
|
382
|
-
document.addEventListener('visibilitychange', () => {
|
|
386
|
+
document.addEventListener('visibilitychange', () => {
|
|
387
|
+
if (!document.hidden) {
|
|
388
|
+
refreshActive();
|
|
389
|
+
// Two GUI tabs on the Live view can silently diverge on sort/filter -
|
|
390
|
+
// re-read the shared prefs key on refocus so a change made in another
|
|
391
|
+
// tab is reconciled instead of staying invisible until a hard reload.
|
|
392
|
+
hydratePrefs();
|
|
393
|
+
render();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
383
396
|
}
|
|
384
397
|
}
|
|
385
398
|
function stopActivePolling() {
|
|
@@ -443,6 +456,11 @@ function openLiveStream() {
|
|
|
443
456
|
// replayed: re-baseline. Drop the tally (the index owns truth) and
|
|
444
457
|
// refetch the session list so counters never double-count a replay.
|
|
445
458
|
if (state.live.tally) state.live.tally.clear();
|
|
459
|
+
// A cleared tally momentarily loses the per-sid running-tool/recency
|
|
460
|
+
// signal until the re-fetched index catches up - without a grace
|
|
461
|
+
// window a running session can flap to 'stale' for one tick right
|
|
462
|
+
// after reconnect. Suppress the stale demotion for a few seconds.
|
|
463
|
+
state.live.reconnectGraceUntil = Date.now() + 5000;
|
|
446
464
|
debouncedRefreshHistory();
|
|
447
465
|
}
|
|
448
466
|
} else if (kind === 'event' && data) {
|
|
@@ -755,7 +773,7 @@ function sessionsColumn() {
|
|
|
755
773
|
const hits = (state.searchHits.results || []).slice(0, state.sessionsLimit);
|
|
756
774
|
const items = hits.map((r, i) => ({
|
|
757
775
|
sid: r.sid,
|
|
758
|
-
title: r.snippet || r.title || UNTITLED_CONVERSATION,
|
|
776
|
+
title: r.snippet || projectLabel(r.title) || projectLabel(r.project) || UNTITLED_CONVERSATION,
|
|
759
777
|
project: projectLabel(r.project) || '',
|
|
760
778
|
time: r.ts ? fmtRelTime(r.ts) : '',
|
|
761
779
|
running: false,
|
|
@@ -776,6 +794,7 @@ function sessionsColumn() {
|
|
|
776
794
|
placeholder: 'Search all conversations - titles + messages (2+ chars)',
|
|
777
795
|
onInput: (v) => { state.searchQ = v; if (v.trim().length >= 2) debouncedSearch(); else { state.searchHits = null; } render(); },
|
|
778
796
|
},
|
|
797
|
+
resultCount: state.searchBusy ? null : (hits.length + ' result' + (hits.length === 1 ? '' : 's')),
|
|
779
798
|
onNew: () => { navTo('chat'); newChat(); },
|
|
780
799
|
onSelect: (s) => {
|
|
781
800
|
if (state.tab === 'chat') resumeInChat({ sid: s.sid });
|
|
@@ -844,7 +863,7 @@ function sessionsColumn() {
|
|
|
844
863
|
loading: !state.sessions.length && !state.historyError,
|
|
845
864
|
loadingText: state.historySlow ? 'Indexing your Claude history — the first load can take a minute…' : undefined,
|
|
846
865
|
error: state.historyError,
|
|
847
|
-
emptyText: 'No conversations yet',
|
|
866
|
+
emptyText: 'No conversations yet - start one from Chat',
|
|
848
867
|
hasMore: sessionsView.length > state.sessionsLimit,
|
|
849
868
|
onLoadMore: () => { state.sessionsLimit += 60; render(); },
|
|
850
869
|
});
|
|
@@ -1660,6 +1679,12 @@ async function stopAllActive(sessions) {
|
|
|
1660
1679
|
announce(failed
|
|
1661
1680
|
? 'stopped ' + okSids.length + ' of ' + sids.length + ' conversations'
|
|
1662
1681
|
: 'stopped ' + plural(okSids.length, 'conversation'));
|
|
1682
|
+
// Clear EVERY attempted sid from the stopping set - both ok and failed -
|
|
1683
|
+
// mirroring stopActiveChat's catch-block cleanup. A failed cancel that stays
|
|
1684
|
+
// in state.active would otherwise never leave `stopping` (refreshActive only
|
|
1685
|
+
// clears sids that vanished from the active set), leaving its stop button
|
|
1686
|
+
// permanently disabled instead of retryable.
|
|
1687
|
+
for (const sid of sids) st.delete(sid);
|
|
1663
1688
|
await refreshActive();
|
|
1664
1689
|
render();
|
|
1665
1690
|
return okSids;
|
|
@@ -1716,15 +1741,22 @@ function computeLiveSessions() {
|
|
|
1716
1741
|
// Status reflects CURRENT reality: error only when an error is recent (a
|
|
1717
1742
|
// recovered tool error hours ago is history, kept in the counter chip);
|
|
1718
1743
|
// stale = no recent activity AND no running tool.
|
|
1744
|
+
const inReconnectGrace = now < (state.live.reconnectGraceUntil || 0);
|
|
1719
1745
|
let status = 'running';
|
|
1720
1746
|
if (lastErrorTs && (nowS - lastErrorTs) <= STALE_AFTER_MS) status = 'error';
|
|
1721
|
-
else if (!currentTool && lastTs && (nowS - lastTs) > STALE_AFTER_MS) status = 'stale';
|
|
1747
|
+
else if (!currentTool && lastTs && (nowS - lastTs) > STALE_AFTER_MS && !inReconnectGrace) status = 'stale';
|
|
1722
1748
|
const startedTs = r.startedAt || 0;
|
|
1723
1749
|
const elapsedMs = startedTs ? Math.max(0, now - startedTs) : 0;
|
|
1724
1750
|
// One title source shared with the rails: projectLabel(title|project)|sid.
|
|
1751
|
+
// A brand-new session (started <3s ago) with no index row yet is still
|
|
1752
|
+
// BEING indexed, not permanently unresolved - give it a distinct
|
|
1753
|
+
// "indexing..." placeholder instead of rendering identically to a session
|
|
1754
|
+
// whose index lookup genuinely never resolves.
|
|
1755
|
+
const startedTsForTitle = r.startedAt || 0;
|
|
1756
|
+
const stillIndexing = !sess && startedTsForTitle && (now - startedTsForTitle < 3000);
|
|
1725
1757
|
const title = sess
|
|
1726
1758
|
? (projectLabel(sess.title) || projectLabel(sess.project) || realSid)
|
|
1727
|
-
: (r.claudeSessionId ? realSid : '');
|
|
1759
|
+
: (stillIndexing ? 'indexing…' : (r.claudeSessionId ? realSid : ''));
|
|
1728
1760
|
return {
|
|
1729
1761
|
sid: r.sessionId,
|
|
1730
1762
|
realSid,
|
|
@@ -1792,7 +1824,7 @@ function computeLiveSessions() {
|
|
|
1792
1824
|
// owned vs external session in the identical objective state must read
|
|
1793
1825
|
// the same status.
|
|
1794
1826
|
status: (lastErrorTs && (nowS - lastErrorTs) <= STALE_AFTER_MS) ? 'error'
|
|
1795
|
-
: (!t.toolRunning && t.last && (nowS - t.last) > STALE_AFTER_MS ? 'stale' : 'running'),
|
|
1827
|
+
: (!t.toolRunning && t.last && (nowS - t.last) > STALE_AFTER_MS && now >= (state.live.reconnectGraceUntil || 0) ? 'stale' : 'running'),
|
|
1796
1828
|
stopping: false,
|
|
1797
1829
|
});
|
|
1798
1830
|
}
|
|
@@ -2404,7 +2436,13 @@ function restoreChat() {
|
|
|
2404
2436
|
const raw = lsGet(CHAT_KEY);
|
|
2405
2437
|
if (!raw) return;
|
|
2406
2438
|
const saved = JSON.parse(raw);
|
|
2407
|
-
if (typeof saved.draft === 'string' && saved.draft.trim())
|
|
2439
|
+
if (typeof saved.draft === 'string' && saved.draft.trim()) {
|
|
2440
|
+
state.chat.draft = saved.draft;
|
|
2441
|
+
// A restored draft is set programmatically, so Ctrl+Z has nothing in the
|
|
2442
|
+
// browser's native undo history to act on - a one-time note explains why
|
|
2443
|
+
// undo appears to do nothing right after a reload.
|
|
2444
|
+
announce('draft restored - note: undo history does not carry over a reload');
|
|
2445
|
+
}
|
|
2408
2446
|
if (typeof saved.totalCost === 'number') state.chat.totalCost = saved.totalCost;
|
|
2409
2447
|
if (Array.isArray(saved.messages) && saved.messages.length) {
|
|
2410
2448
|
state.chat.messages = saved.messages
|
|
@@ -2440,9 +2478,9 @@ function messageToText(m) {
|
|
|
2440
2478
|
// aria-live region so AT users hear the result.
|
|
2441
2479
|
function copyText(text, msg) {
|
|
2442
2480
|
if (!text) return;
|
|
2443
|
-
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(() => announce(msg)).catch(() =>
|
|
2481
|
+
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(() => announce(msg)).catch(() => announce('copy failed'));
|
|
2444
2482
|
else {
|
|
2445
|
-
try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); announce(msg); } catch {}
|
|
2483
|
+
try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); announce(msg); } catch { announce('copy failed'); }
|
|
2446
2484
|
}
|
|
2447
2485
|
}
|
|
2448
2486
|
function copyMessageText(m) {
|
|
@@ -2806,7 +2844,7 @@ function historyMain() {
|
|
|
2806
2844
|
const head = PageHeader({
|
|
2807
2845
|
compact: true,
|
|
2808
2846
|
dense: true,
|
|
2809
|
-
title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || sess?.cwd || UNTITLED_CONVERSATION, 40, 80),
|
|
2847
|
+
title: truncate(projectLabel(sess?.title) || projectLabel(sess?.project) || sess?.cwd || state.selectedSid || UNTITLED_CONVERSATION, 40, 80),
|
|
2810
2848
|
lede,
|
|
2811
2849
|
});
|
|
2812
2850
|
|
|
@@ -2898,6 +2936,12 @@ function historyMain() {
|
|
|
2898
2936
|
hiddenCount > 0
|
|
2899
2937
|
? Btn({ key: 'older', onClick: () => { const added = Math.min(300, hiddenCount); state.eventsLimit += 300; announce('loaded ' + added + ' more events'); render(); }, children: 'load ' + Math.min(300, hiddenCount) + ' older (' + hiddenCount + ' hidden)' })
|
|
2900
2938
|
: null,
|
|
2939
|
+
// A per-click 300-event step is fine for casual scanning, but a huge
|
|
2940
|
+
// session (thousands hidden) makes that a lot of repeat clicks - a
|
|
2941
|
+
// secondary "load all" jumps straight to the full transcript.
|
|
2942
|
+
hiddenCount > 1000
|
|
2943
|
+
? Btn({ key: 'loadall', onClick: () => { state.eventsLimit = total; announce('loaded all ' + total + ' events'); render(); }, children: 'load all (' + hiddenCount + ' hidden)' })
|
|
2944
|
+
: null,
|
|
2901
2945
|
);
|
|
2902
2946
|
return [
|
|
2903
2947
|
reconnectAlert(),
|
package/site/app/js/backend.js
CHANGED
|
@@ -190,15 +190,50 @@ export async function searchHistory(base, q, limit = 50) {
|
|
|
190
190
|
return r.json();
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// Native EventSource auto-reconnects on its own schedule with no cap, unlike
|
|
194
|
+
// the WS client's explicit exponential backoff below - a flaky proxy can spin
|
|
195
|
+
// it into a tight reconnect loop. Wrap the lifecycle with the same manual
|
|
196
|
+
// close+reopen-on-timer backoff (start 500ms, double, cap 30000ms, reset on a
|
|
197
|
+
// successful connection) so both realtime transports behave consistently.
|
|
198
|
+
// Returns a thin handle exposing addEventListener('error', ...)/close(), the
|
|
199
|
+
// same surface callers already use against a raw EventSource.
|
|
193
200
|
export function streamHistory(base, onEvent) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
const EVENT_KINDS = ['hello', 'event', 'error', 'start', 'complete', 'conversation'];
|
|
202
|
+
let es = null;
|
|
203
|
+
let closed = false;
|
|
204
|
+
let attempts = 0;
|
|
205
|
+
let retryTimer = null;
|
|
206
|
+
const errorListeners = new Set();
|
|
207
|
+
|
|
208
|
+
function connect() {
|
|
209
|
+
if (closed) return;
|
|
210
|
+
es = new EventSource(withToken(base + '/v1/history/stream'));
|
|
211
|
+
for (const k of EVENT_KINDS) {
|
|
212
|
+
es.addEventListener(k, ev => {
|
|
213
|
+
if (k === 'hello') { attempts = 0; } // reset backoff on a live connection
|
|
214
|
+
let data; try { data = JSON.parse(ev.data); } catch { data = null; }
|
|
215
|
+
onEvent(k, data);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
es.addEventListener('error', () => {
|
|
219
|
+
if (closed) return;
|
|
220
|
+
try { es.close(); } catch {}
|
|
221
|
+
for (const fn of errorListeners) { try { fn(); } catch {} }
|
|
222
|
+
const delay = Math.min(30000, 500 * Math.pow(2, attempts));
|
|
223
|
+
attempts++;
|
|
224
|
+
retryTimer = setTimeout(() => { retryTimer = null; connect(); }, delay);
|
|
199
225
|
});
|
|
200
226
|
}
|
|
201
|
-
|
|
227
|
+
connect();
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
addEventListener(kind, fn) { if (kind === 'error') errorListeners.add(fn); },
|
|
231
|
+
close() {
|
|
232
|
+
closed = true;
|
|
233
|
+
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
|
|
234
|
+
if (es) { try { es.close(); } catch {} }
|
|
235
|
+
},
|
|
236
|
+
};
|
|
202
237
|
}
|
|
203
238
|
|
|
204
239
|
// ---------- WebSocket client (/sync) ----------
|