agentgui 1.0.1062 → 1.0.1063
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 +0 -9
- package/package.json +1 -1
- package/site/app/js/app.js +65 -4
package/.gm/prd.yml
CHANGED
|
@@ -3109,15 +3109,6 @@
|
|
|
3109
3109
|
- id: gc-delete-undo-trash
|
|
3110
3110
|
subject: No undo-after-delete (soft-delete/trash) anywhere in the confined delete surface
|
|
3111
3111
|
status: pending
|
|
3112
|
-
- id: gc-search-hit-hash-anchor
|
|
3113
|
-
subject: Search-hit event anchor (focusEventI/focusEventTs) not carried in the hash - reload/Back loses highlighted line
|
|
3114
|
-
status: pending
|
|
3115
|
-
- id: gc-settings-section-scrollspy
|
|
3116
|
-
subject: Settings section anchor is deep-link-in only - scrolling never updates state.settingsSection or the URL
|
|
3117
|
-
status: pending
|
|
3118
|
-
- id: gc-context-budget-affordance
|
|
3119
|
-
subject: No context-size/token-budget affordance - only turn count and dollar cost shown, never remaining context headroom
|
|
3120
|
-
status: pending
|
|
3121
3112
|
- id: gc-expanded-body-highlight
|
|
3122
3113
|
subject: Search-result highlighting inside expanded event bodies never highlights the matched query term
|
|
3123
3114
|
status: pending
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -51,7 +51,7 @@ const ARM_RESET_MS = 4000;
|
|
|
51
51
|
|
|
52
52
|
// Full routable param set. Every view-defining piece of state round-trips
|
|
53
53
|
// through the hash so reload and Back/forward restore the exact view.
|
|
54
|
-
const HASH_KEYS = ['tab', 'sid', 'dir', 'file', 'q', 'project', 'section', 'filter', 'lsort', 'lfilter', 'lerr'];
|
|
54
|
+
const HASH_KEYS = ['tab', 'sid', 'dir', 'file', 'q', 'project', 'section', 'filter', 'lsort', 'lfilter', 'lerr', 'ets'];
|
|
55
55
|
function readHash() {
|
|
56
56
|
const hash = location.hash || '';
|
|
57
57
|
const out = {};
|
|
@@ -80,6 +80,11 @@ function buildHash() {
|
|
|
80
80
|
const q = (state.searchQ || '').trim();
|
|
81
81
|
if (q.length >= 2) parts.push('q=' + encodeURIComponent(q));
|
|
82
82
|
if (state.projectFilter) parts.push('project=' + encodeURIComponent(state.projectFilter));
|
|
83
|
+
// A session opened from a search hit carries the matched event's
|
|
84
|
+
// timestamp so reload/Back reproduces the same scrolled+flashed position
|
|
85
|
+
// a live click gives - without this, the anchor only existed in memory
|
|
86
|
+
// and a search-hit URL degraded to just the bare session on reload.
|
|
87
|
+
if (state._focusEventTs != null) parts.push('ets=' + encodeURIComponent(state._focusEventTs));
|
|
83
88
|
}
|
|
84
89
|
if (tab === 'settings' && state.settingsSection) parts.push('section=' + encodeURIComponent(state.settingsSection));
|
|
85
90
|
if (tab === 'live') {
|
|
@@ -822,7 +827,14 @@ function sessionsColumn() {
|
|
|
822
827
|
onNew: () => { navTo('chat'); newChat(); },
|
|
823
828
|
onSelect: (s) => {
|
|
824
829
|
if (state.tab === 'chat') resumeInChat({ sid: s.sid });
|
|
825
|
-
else
|
|
830
|
+
else {
|
|
831
|
+
// Persist the anchor so buildHash() can carry it into the URL -
|
|
832
|
+
// without this, reload/Back only ever restored the bare session,
|
|
833
|
+
// losing the matched event's scroll+flash position.
|
|
834
|
+
state._focusEventTs = s._focusEventTs ?? null;
|
|
835
|
+
loadSession(s.sid, { focusEventI: s._focusEventI, focusEventTs: s._focusEventTs });
|
|
836
|
+
writeHash({ push: true });
|
|
837
|
+
}
|
|
826
838
|
},
|
|
827
839
|
loading: state.searchBusy,
|
|
828
840
|
error: state.searchHits.error || null,
|
|
@@ -3927,6 +3939,10 @@ async function loadSession(sid, { focusEventI = null, focusEventTs = null, fromH
|
|
|
3927
3939
|
return;
|
|
3928
3940
|
}
|
|
3929
3941
|
state.selectedSid = sid;
|
|
3942
|
+
// A plain (non-search-hit) session open must not carry a stale event
|
|
3943
|
+
// anchor forward into the URL - only reset it when this call ISN'T itself
|
|
3944
|
+
// the one supplying a fresh focusEventTs.
|
|
3945
|
+
if (focusEventTs == null) state._focusEventTs = null;
|
|
3930
3946
|
state.events = [];
|
|
3931
3947
|
state.events._seen = new Set(); // O(1) dedupe by event index
|
|
3932
3948
|
state.eventsLoaded = false;
|
|
@@ -4092,9 +4108,11 @@ async function init() {
|
|
|
4092
4108
|
} else if (hp.sid) {
|
|
4093
4109
|
if (hp.q) state.searchQ = hp.q;
|
|
4094
4110
|
if (hp.project) state.projectFilter = hp.project;
|
|
4111
|
+
const bootFocusTs = hp.ets != null ? Number(hp.ets) : null;
|
|
4112
|
+
if (bootFocusTs != null && !Number.isNaN(bootFocusTs)) state._focusEventTs = bootFocusTs;
|
|
4095
4113
|
navTo('history', { push: false });
|
|
4096
4114
|
await refreshHistory();
|
|
4097
|
-
await loadSession(hp.sid, { fromHash: true });
|
|
4115
|
+
await loadSession(hp.sid, { fromHash: true, focusEventTs: bootFocusTs != null && !Number.isNaN(bootFocusTs) ? bootFocusTs : undefined });
|
|
4098
4116
|
if (state.searchQ.trim().length >= 2) runSearch();
|
|
4099
4117
|
} else if (bootTab !== state.tab) {
|
|
4100
4118
|
// Files deep-link: restore the directory the URL names (reload keeps
|
|
@@ -4119,6 +4137,7 @@ async function init() {
|
|
|
4119
4137
|
}
|
|
4120
4138
|
|
|
4121
4139
|
registerWsStatusOnce();
|
|
4140
|
+
registerSettingsScrollSpyOnce();
|
|
4122
4141
|
startActivePolling(); // surface running chats on any tab, not just history
|
|
4123
4142
|
startRelTimeTick();
|
|
4124
4143
|
startLiveTick(); // 1s elapsed advance on the live dashboard
|
|
@@ -4177,6 +4196,44 @@ function focusSettingsSection(id) {
|
|
|
4177
4196
|
el.addEventListener('blur', clear);
|
|
4178
4197
|
});
|
|
4179
4198
|
}
|
|
4199
|
+
const SETTINGS_SECTION_IDS = ['backend', 'server', 'agents', 'appearance', 'keyboard', 'data'];
|
|
4200
|
+
// Settings was deep-link-IN only: focusSettingsSection() (a section= URL param
|
|
4201
|
+
// or the ?-overlay jump) set state.settingsSection, but manually scrolling
|
|
4202
|
+
// never updated it back - so the URL/state silently went stale the instant a
|
|
4203
|
+
// user scrolled by hand, and Back could never step BETWEEN panels the way it
|
|
4204
|
+
// does for every other tab. A lightweight scroll-position scrollspy (not a
|
|
4205
|
+
// full IntersectionObserver - the settings scroll region is small/short-lived
|
|
4206
|
+
// enough that a debounced scroll-position check is simpler and avoids the
|
|
4207
|
+
// observer-lifecycle bookkeeping) keeps state.settingsSection (and the URL)
|
|
4208
|
+
// honest while the user scrolls, registered once like the WS/session-expired
|
|
4209
|
+
// listeners above.
|
|
4210
|
+
const debouncedSettingsScrollSpy = debounce(() => {
|
|
4211
|
+
if (state.tab !== 'settings') return;
|
|
4212
|
+
const region = document.querySelector('#agentgui-main');
|
|
4213
|
+
if (!region) return;
|
|
4214
|
+
const regionTop = region.getBoundingClientRect().top;
|
|
4215
|
+
let current = null;
|
|
4216
|
+
for (const id of SETTINGS_SECTION_IDS) {
|
|
4217
|
+
const el = document.getElementById(id);
|
|
4218
|
+
if (!el) continue;
|
|
4219
|
+
// The section whose top has scrolled past the region's own top edge
|
|
4220
|
+
// (with a little slack for the sticky header) is the "current" one -
|
|
4221
|
+
// same heuristic every scrollspy implementation uses.
|
|
4222
|
+
if (el.getBoundingClientRect().top - regionTop <= 80) current = id;
|
|
4223
|
+
}
|
|
4224
|
+
if (current && current !== state.settingsSection) {
|
|
4225
|
+
state.settingsSection = current;
|
|
4226
|
+
writeHash({ push: false }); // passive sync, not a Back-able step - matches search text/filter's replaceState treatment
|
|
4227
|
+
}
|
|
4228
|
+
}, 150);
|
|
4229
|
+
let settingsScrollSpyRegistered = false;
|
|
4230
|
+
function registerSettingsScrollSpyOnce() {
|
|
4231
|
+
if (settingsScrollSpyRegistered) return;
|
|
4232
|
+
settingsScrollSpyRegistered = true;
|
|
4233
|
+
document.addEventListener('scroll', (e) => {
|
|
4234
|
+
if (e.target && e.target.id === 'agentgui-main') debouncedSettingsScrollSpy();
|
|
4235
|
+
}, true); // capture: #agentgui-main itself is the scrolling element, not a bubling target
|
|
4236
|
+
}
|
|
4180
4237
|
|
|
4181
4238
|
// Browser Back/forward: diff the FULL hash param set against state and re-sync
|
|
4182
4239
|
// each piece. Everything here runs with writeHash:false / fromHash:true so the
|
|
@@ -4189,7 +4246,11 @@ window.addEventListener('popstate', () => {
|
|
|
4189
4246
|
// anything else - or a bare sid - opens it in history).
|
|
4190
4247
|
if (hp.sid && hp.sid !== state.selectedSid) {
|
|
4191
4248
|
if (tab === 'chat') resumeInChat({ sid: hp.sid }, { fromHash: true });
|
|
4192
|
-
else
|
|
4249
|
+
else {
|
|
4250
|
+
const popFocusTs = hp.ets != null ? Number(hp.ets) : null;
|
|
4251
|
+
state._focusEventTs = (popFocusTs != null && !Number.isNaN(popFocusTs)) ? popFocusTs : null;
|
|
4252
|
+
loadSession(hp.sid, { fromHash: true, focusEventTs: (popFocusTs != null && !Number.isNaN(popFocusTs)) ? popFocusTs : undefined });
|
|
4253
|
+
}
|
|
4193
4254
|
} else if (!hp.sid && state.selectedSid && tab === 'history') {
|
|
4194
4255
|
state.selectedSid = null;
|
|
4195
4256
|
state.events = [];
|