agentgui 1.0.1042 → 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-e490a181de6469fc-487.md +8 -0
- package/.gm/memories/mem-fe3faa1652f07239-480.md +8 -0
- 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-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/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)));
|