@oddessentials/agent-guild 1.18.0 → 1.19.0

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/web/app.js CHANGED
@@ -2293,8 +2293,8 @@ function buildProvider(provider) {
2293
2293
  existing.addEventListener('click', (event) => showHistory(provider, false, event.currentTarget));
2294
2294
  node.querySelector('.launch-flags').addEventListener('click', (event) => showFlags(provider, event.currentTarget));
2295
2295
  const memory = node.querySelector('.memory-link');
2296
- memory.hidden = !provider.available || (!provider.memorySource && !provider.historyDetails);
2297
- const historyLink = !provider.memorySource && provider.historyDetails;
2296
+ const historyLink = !provider.memorySource && provider.historySource === 'antigravity';
2297
+ memory.hidden = !provider.available || (!provider.memorySource && !historyLink);
2298
2298
  memory.textContent = historyLink ? 'History' : 'Memory';
2299
2299
  memory.title = historyLink ? `Read ${provider.tool}'s saved conversations` : `See what ${provider.tool} remembers about the working folder`;
2300
2300
  memory.addEventListener('click', (event) => historyLink ? showHistory(provider, true, event.currentTarget) : showMemory(provider, event.currentTarget));
@@ -3970,7 +3970,7 @@ async function startSession(provider, card, { resume, cwd, account = selectedAcc
3970
3970
  ({ session } = await api('POST', '/sessions', body));
3971
3971
  } catch (err) {
3972
3972
  if (err.code !== 'bad_cwd' || !cwd) throw err;
3973
- if (resume && provider.historyDetails) {
3973
+ if (resume && provider.historySource === 'antigravity') {
3974
3974
  const chosen = (await ask(`${cwd} no longer exists. Choose a working folder to resume this conversation:`, { ok: 'Resume', value: working }))?.trim();
3975
3975
  if (!chosen) return;
3976
3976
  ({ session } = await api('POST', '/sessions', { ...body, cwd: chosen }));
@@ -4011,20 +4011,22 @@ function renderHistoryPreview(view) {
4011
4011
  $('history-preview-title').textContent = view.target?.title || 'Select a conversation';
4012
4012
  $('history-preview-refresh').disabled = !view.target || view.loading;
4013
4013
  $('history-preview-more').hidden = !view.nextCursor;
4014
- paintPending($('history-preview-more'), view.loading, 'Read more', 'Reading…');
4014
+ paintPending($('history-preview-more'), view.loading, 'Read earlier', 'Reading…');
4015
4015
  const notes = [];
4016
4016
  if (view.error) notes.push(view.error);
4017
4017
  else if (view.loading) notes.push('Reading saved messages…');
4018
- else if (view.target && !view.messages.length) notes.push(view.nextCursor ? 'No visible messages in this part. Continue reading below.' : 'No complete visible messages in this part.');
4018
+ else if (view.target && !view.messages.length) notes.push(view.nextCursor ? 'No visible messages in this part. Read earlier messages above.' : 'No visible messages in this conversation.');
4019
4019
  else if (!view.target) notes.push('Read a saved exchange, then resume when you want to continue it.');
4020
- if (view.omitted) notes.push('Some unreadable or oversized records were omitted.');
4020
+ if (view.omitted) notes.push('Some unreadable records were omitted.');
4021
+ if (view.truncated) notes.push('Earlier messages cannot be shown: a record before these is too large or unreadable.');
4021
4022
  if (view.incomplete) notes.push('The last record is incomplete. Refresh after the conversation finishes writing.');
4022
4023
  $('history-preview-note').textContent = notes.join(' ');
4023
4024
  const messages = $('history-messages');
4024
- const scroll = messages.scrollTop;
4025
+ const fromBottom = messages.scrollHeight - messages.scrollTop;
4026
+ const first = messages.childElementCount === 0;
4025
4027
  messages.replaceChildren(...view.messages.map((message) => el('article', 'history-message',
4026
4028
  el('h4', null, message.role === 'user' ? 'You' : 'Assistant'), el('p', null, message.text))));
4027
- messages.scrollTop = view.messages.length ? scroll : 0;
4029
+ messages.scrollTop = first ? messages.scrollHeight : messages.scrollHeight - fromBottom;
4028
4030
  for (const node of $('history-list').querySelectorAll('.history-preview-open')) node.setAttribute('aria-current', String(node.dataset.id === view.target?.id));
4029
4031
  }
4030
4032
 
@@ -4090,10 +4092,8 @@ async function loadHistory() {
4090
4092
  renderHistory();
4091
4093
  try {
4092
4094
  const query = new URLSearchParams({ account: accountId, limit: HISTORY_LIMIT });
4093
- if (provider.historyDetails) {
4094
- if ($('history-here').checked && $('cwd').value.trim()) query.set('cwd', $('cwd').value.trim());
4095
- if ($('history-filter').value.trim()) query.set('q', $('history-filter').value.trim());
4096
- }
4095
+ if ($('history-here').checked && $('cwd').value.trim()) query.set('cwd', $('cwd').value.trim());
4096
+ if ($('history-filter').value.trim()) query.set('q', $('history-filter').value.trim());
4097
4097
  const { history } = await api('GET', `/providers/${providerId}/history?${query}`);
4098
4098
  if (request !== historyView.request) return;
4099
4099
  historyView.snapshot = history;
@@ -4107,27 +4107,18 @@ async function loadHistory() {
4107
4107
  }
4108
4108
 
4109
4109
  function historyFiltersChanged() {
4110
- if (historyProvider()?.historyDetails) {
4110
+ if (historyProvider()?.historySource) {
4111
4111
  historyView.snapshot = null;
4112
4112
  historyPreview.clear();
4113
4113
  loadHistory();
4114
4114
  } else renderHistory();
4115
4115
  }
4116
4116
 
4117
- function historyText(entry) {
4118
- return `${entry.title ?? ''}\n${entry.cwd ?? ''}\n${entry.id}`.toLowerCase();
4119
- }
4120
-
4121
4117
  function folderName(dir) {
4122
4118
  const parts = dir.replace(/[\\/]+$/, '').split(/[\\/]/);
4123
4119
  return parts.at(-1) || dir;
4124
4120
  }
4125
4121
 
4126
- function sameFolder(a, b) {
4127
- const clean = (dir) => (dir || '').replace(/[\\/]+$/, '').replace(/\\/g, '/').toLowerCase();
4128
- return clean(a) === clean(b);
4129
- }
4130
-
4131
4122
  function historyEntry(id) {
4132
4123
  return historyView.snapshot?.sessions.find((entry) => entry.id === id) ?? null;
4133
4124
  }
@@ -4138,7 +4129,7 @@ async function resumeFromHistory(provider, id, cwd) {
4138
4129
  closeHistory({ focusOpener: false });
4139
4130
  openPanel(running.id);
4140
4131
  } else {
4141
- if (provider.historyDetails && !cwd) {
4132
+ if (provider.historySource === 'antigravity' && !cwd) {
4142
4133
  const workspaces = historyEntry(id)?.workspaces ?? [];
4143
4134
  const reason = workspaces.length > 1 ? `This conversation has several workspaces:\n${workspaces.join('\n')}\n\n` : 'The saved working folder is unknown.\n\n';
4144
4135
  cwd = (await ask(`${reason}Choose a working folder to resume in:`, { ok: 'Resume', value: $('cwd').value.trim() }))?.trim();
@@ -4164,27 +4155,28 @@ function buildHistoryRow(id) {
4164
4155
 
4165
4156
  function updateHistoryRow(node, provider, entry) {
4166
4157
  const running = runningOn(provider.id, historyView.accountId, entry.id);
4167
- node.classList.toggle('untitled', !entry.title);
4158
+ const label = entry.name ?? entry.title;
4159
+ node.classList.toggle('untitled', !label);
4168
4160
  node.classList.toggle('running', Boolean(running));
4169
4161
  const title = node.querySelector('.history-title');
4170
- title.title = entry.title ?? '';
4162
+ title.title = label ?? '';
4171
4163
  if (provider.historyDetails) {
4172
4164
  let preview = title.querySelector('button');
4173
4165
  if (!preview) {
4174
4166
  preview = button('', () => {
4175
4167
  const current = historyEntry(entry.id);
4176
- if (current) historyPreview.select({ providerId: provider.id, accountId: historyView.accountId, id: entry.id, title: current.title ?? 'Untitled session' });
4168
+ if (current) historyPreview.select({ providerId: provider.id, accountId: historyView.accountId, id: entry.id, title: current.name ?? current.title ?? 'Untitled session' });
4177
4169
  }, 'history-preview-open');
4178
4170
  preview.dataset.id = entry.id;
4179
4171
  title.replaceChildren(preview);
4180
4172
  }
4181
- preview.textContent = entry.title ?? 'Untitled session';
4173
+ preview.textContent = label ?? 'Untitled session';
4182
4174
  preview.setAttribute('aria-current', String(historyPreview.state.target?.id === entry.id));
4183
- } else title.textContent = entry.title ?? 'Untitled session';
4175
+ } else title.textContent = label ?? 'Untitled session';
4184
4176
  const meta = node.querySelector('.history-meta');
4185
4177
  const when = entry.updatedAt ? `updated ${relativeTime(entry.updatedAt)}` : '';
4186
- const folder = entry.cwd ? folderName(entry.cwd) : provider.historyDetails ? (entry.workspaces?.length > 1 ? `${entry.workspaces.length} workspaces` : 'Folder unknown') : null;
4187
- meta.textContent = [folder, when, running && `open in Agent Guild as ${running.name}`].filter(Boolean).join(' · ');
4178
+ const folder = entry.cwd ? folderName(entry.cwd) : provider.historySource === 'antigravity' ? (entry.workspaces?.length > 1 ? `${entry.workspaces.length} workspaces` : 'Folder unknown') : null;
4179
+ meta.textContent = [entry.name && entry.title !== entry.name ? entry.title : null, folder, when, running && `open in Agent Guild as ${running.name}`].filter(Boolean).join(' · ');
4188
4180
  meta.title = [...(entry.workspaces ?? [entry.cwd]), entry.startedAt && `started ${new Date(entry.startedAt).toLocaleString()}`].filter(Boolean).join('\n');
4189
4181
  const action = node.querySelector('.history-resume');
4190
4182
  const key = sessionActionKey(provider.id, historyView.accountId, entry.id);
@@ -4193,7 +4185,7 @@ function updateHistoryRow(node, provider, entry) {
4193
4185
  action.title = running
4194
4186
  ? `This session is running in Agent Guild as "${running.name}". Open it instead of resuming it twice.`
4195
4187
  : `Resume this ${provider.tool} session${entry.cwd ? ` in ${entry.cwd}` : ''}`;
4196
- action.setAttribute('aria-label', `${action.textContent} ${entry.title ?? entry.id}`);
4188
+ action.setAttribute('aria-label', `${action.textContent} ${label ?? entry.id}`);
4197
4189
  }
4198
4190
 
4199
4191
  /** Rows are kept and updated in place, so a refresh never drops keyboard focus from a row's buttons. */
@@ -4222,9 +4214,8 @@ function renderHistory() {
4222
4214
  const here = $('history-here');
4223
4215
  here.disabled = !working;
4224
4216
  here.parentElement.title = working ? `Only sessions started in ${working}` : 'Set a working folder above to filter by it';
4225
- const serverFiltered = provider.historyDetails && Boolean(filter || (here.checked && !here.disabled));
4226
- const all = snapshot?.sessions ?? [];
4227
- const shown = provider.historyDetails ? all : all.filter((entry) => (!filter || historyText(entry).includes(filter)) && (!here.checked || here.disabled || sameFolder(entry.cwd, working)));
4217
+ const serverFiltered = Boolean(filter || (here.checked && !here.disabled));
4218
+ const shown = snapshot?.sessions ?? [];
4228
4219
  const parts = [];
4229
4220
  if ((provider.accounts?.length ?? 0) > 1 && account) parts.push(`${account.label} account`);
4230
4221
  if (snapshot && !snapshot.error) {
@@ -4239,14 +4230,11 @@ function renderHistory() {
4239
4230
  if (!provider.historySource) note = `Agent Guild cannot list ${provider.tool}'s sessions. Enter the id of one to resume it.`;
4240
4231
  else if (historyView.loading && !snapshot) note = `Reading ${provider.tool}'s sessions…`;
4241
4232
  else if (snapshot?.error) note = `Sessions could not be read: ${snapshot.error}`;
4242
- else if (snapshot && all.length === 0 && !serverFiltered) note = `No ${provider.tool} sessions were found${account && account.id !== 'default' ? ` for the ${account.label} account` : ''}.`;
4233
+ else if (snapshot && shown.length === 0 && !serverFiltered) note = `No ${provider.tool} sessions were found${account && account.id !== 'default' ? ` for the ${account.label} account` : ''}.`;
4243
4234
  else if (snapshot && shown.length === 0) note = 'No session matches the filter.';
4244
4235
  if (snapshot?.note) note = [note, snapshot.note].filter(Boolean).join(' ');
4245
- if (snapshot?.total > all.length) {
4246
- const limitNote = provider.historyDetails
4247
- ? `Showing ${all.length} of ${snapshot.total} sessions. Narrow the filter to find older conversations.`
4248
- : `Loaded the newest ${all.length} of ${snapshot.total} sessions. Filtering searches these loaded sessions.`;
4249
- note = [note, limitNote].filter(Boolean).join(' ');
4236
+ if (snapshot?.total > shown.length) {
4237
+ note = [note, `Showing ${shown.length} of ${snapshot.total} sessions. Narrow the filter to find older conversations.`].filter(Boolean).join(' ');
4250
4238
  }
4251
4239
  $('history-note').textContent = note;
4252
4240
  $('history-note').hidden = !note;
@@ -5305,6 +5293,7 @@ async function loadView(view) {
5305
5293
  const request = { key, stamp, value: before?.stamp === stamp ? before.value : null, error: null, loading: true };
5306
5294
  githubPick.data[view] = request;
5307
5295
  renderGitHubViews();
5296
+ const watching = view === 'actions' ? new Set(runWatch.runs.values()) : null;
5308
5297
  let value = null;
5309
5298
  let error = null;
5310
5299
  try {
@@ -5319,7 +5308,7 @@ async function loadView(view) {
5319
5308
  renderGitHubViews();
5320
5309
  if (view === 'actions') scheduleRuns();
5321
5310
  if (view === 'actions' && value) {
5322
- runWatchSeen(repo, value.runs);
5311
+ runWatchSeen(repo, value.runs, watching);
5323
5312
  deliverRunWatch();
5324
5313
  }
5325
5314
  }
@@ -5556,11 +5545,12 @@ function endWatch(key, outcome) {
5556
5545
  runWatch.outcomes.push({ repo: watch.repo.fullName, name: watch.name, title: watch.title, url: watch.url, ...outcome });
5557
5546
  }
5558
5547
 
5559
- function runWatchSeen(repo, runs) {
5548
+ /** Only the watches there were when the request for `runs` began: a later one may be for a run newer than those listed. */
5549
+ function runWatchSeen(repo, runs, watching = new Set(runWatch.runs.values())) {
5560
5550
  const key = repoKey(repo);
5561
5551
  let changed = false;
5562
5552
  for (const [id, watch] of runWatch.runs) {
5563
- if (repoKey(watch.repo) !== key) continue;
5553
+ if (repoKey(watch.repo) !== key || !watching.has(watch)) continue;
5564
5554
  const run = runs.find((r) => r.id === watch.id);
5565
5555
  if (run && run.status !== 'completed') continue;
5566
5556
  changed = true;
@@ -5635,12 +5625,13 @@ function pollRunWatch() {
5635
5625
  runWatch.polling = (async () => {
5636
5626
  if (!state.connected || !runWatch.runs.size) return;
5637
5627
  if (!state.github) await api('GET', '/github').then(({ github }) => setGitHub(github), () => {});
5628
+ const watching = new Set(runWatch.runs.values());
5638
5629
  const repos = new Map();
5639
- for (const watch of runWatch.runs.values()) repos.set(repoKey(watch.repo), watch.repo);
5630
+ for (const watch of watching) repos.set(repoKey(watch.repo), watch.repo);
5640
5631
  const accounts = usableGitHubAccounts();
5641
5632
  await Promise.all([...repos.values()].filter((repo) => accounts.some((a) => a.id === repo.accountId)).map(async (repo) => {
5642
5633
  try {
5643
- runWatchSeen(repo, (await api('GET', `${repoPath(repo)}/actions`)).runs);
5634
+ runWatchSeen(repo, (await api('GET', `${repoPath(repo)}/actions`)).runs, watching);
5644
5635
  } catch (err) {
5645
5636
  if (err instanceof AuthError) showAuth(err.message);
5646
5637
  }
@@ -7750,6 +7741,8 @@ function connectEvents() {
7750
7741
  } else if (msg.type === 'yard-art.updated') {
7751
7742
  state.yardArt = msg.yardArt;
7752
7743
  notifyViews();
7744
+ } else if (msg.type === 'yard.fireworks') {
7745
+ yardUi?.celebrate();
7753
7746
  } else if (msg.type === 'news.updated') {
7754
7747
  loadNews();
7755
7748
  } else if (msg.type === 'github.updated') {
@@ -2,10 +2,10 @@
2
2
  // can be exercised with explicitly released promises, without timers or a browser.
3
3
  export function createHistoryPreview({ read, changed, authError = () => {} }) {
4
4
  let request = 0;
5
- const state = { target: null, messages: [], nextCursor: null, loading: false, error: null, incomplete: false, omitted: false };
5
+ const state = { target: null, messages: [], nextCursor: null, loading: false, error: null, incomplete: false, omitted: false, truncated: false };
6
6
  function clear(target = null) {
7
7
  request++;
8
- Object.assign(state, { target, messages: [], nextCursor: null, loading: false, error: null, incomplete: false, omitted: false });
8
+ Object.assign(state, { target, messages: [], nextCursor: null, loading: false, error: null, incomplete: false, omitted: false, truncated: false });
9
9
  changed(state);
10
10
  }
11
11
  async function load(more = false) {
@@ -14,19 +14,19 @@ export function createHistoryPreview({ read, changed, authError = () => {} }) {
14
14
  const target = state.target;
15
15
  const cursor = more ? state.nextCursor : null;
16
16
  Object.assign(state, { loading: true, error: null });
17
- if (!more) Object.assign(state, { messages: [], nextCursor: null, incomplete: false, omitted: false });
17
+ if (!more) Object.assign(state, { messages: [], nextCursor: null, incomplete: false, omitted: false, truncated: false });
18
18
  changed(state);
19
19
  try {
20
20
  const detail = await read(target, cursor);
21
21
  if (request !== ticket) return;
22
- Object.assign(state, { messages: more ? [...state.messages, ...detail.messages] : detail.messages,
23
- nextCursor: detail.nextCursor, incomplete: detail.incomplete, omitted: state.omitted || detail.omitted });
22
+ Object.assign(state, { messages: more ? [...detail.messages, ...state.messages] : detail.messages,
23
+ nextCursor: detail.nextCursor, incomplete: more ? state.incomplete : detail.incomplete, omitted: state.omitted || detail.omitted, truncated: detail.truncated === true });
24
24
  } catch (err) {
25
25
  if (request !== ticket) return;
26
26
  authError(err);
27
27
  state.error = err.message || 'The conversation could not be read.';
28
- // A stale cursor cannot be retried; other errors can retry the same page.
29
- if (err.code === 'history_changed') state.nextCursor = null;
28
+ // A changed conversation drops what was read; other errors can retry the same page.
29
+ if (err.code === 'history_changed') Object.assign(state, { messages: [], nextCursor: null, incomplete: false, omitted: false, truncated: false });
30
30
  } finally {
31
31
  if (request === ticket) { state.loading = false; changed(state); }
32
32
  }
package/web/index.html CHANGED
@@ -650,8 +650,8 @@
650
650
  <button id="history-preview-refresh" class="btn" type="button">Refresh</button>
651
651
  </div>
652
652
  <p id="history-preview-note" class="models-note" role="status"></p>
653
+ <button id="history-preview-more" class="btn" type="button" hidden>Read earlier</button>
653
654
  <div id="history-messages" class="history-messages" tabindex="0" aria-label="Saved messages"></div>
654
- <button id="history-preview-more" class="btn" type="button" hidden>Read more</button>
655
655
  </section>
656
656
  </div>
657
657
  <form id="history-form" class="history-form">