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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1062",
3
+ "version": "1.0.1063",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -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 loadSession(s.sid, { focusEventI: s._focusEventI, focusEventTs: s._focusEventTs });
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 loadSession(hp.sid, { fromHash: true });
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 = [];