@oddessentials/agent-guild 1.17.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/README.md +2 -1
- package/bin/agent-guild.mjs +16 -0
- package/docs/api.md +33 -13
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/package.json +1 -1
- package/src/manager/antigravity-transcript.mjs +4 -70
- package/src/manager/card-names.mjs +110 -0
- package/src/manager/config.mjs +1 -0
- package/src/manager/history-transcripts.mjs +598 -0
- package/src/manager/main.mjs +4 -2
- package/src/manager/providers.mjs +1 -1
- package/src/manager/pty.mjs +26 -0
- package/src/manager/server.mjs +4 -0
- package/src/manager/session-history.mjs +135 -66
- package/src/manager/session-manager.mjs +53 -9
- package/src/manager/session.mjs +21 -18
- package/src/mcp/server.mjs +11 -0
- package/web/app.js +205 -43
- package/web/history-preview.js +7 -7
- package/web/index.html +11 -1
- package/web/styles.css +14 -1
- package/web/yard/art.json +1 -1
- package/web/yard/model.mjs +1 -1
- package/web/yard/renderer.js +45 -12
- package/web/yard/view.js +2 -1
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
|
-
|
|
2297
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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 (
|
|
4094
|
-
|
|
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()?.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
4173
|
+
preview.textContent = label ?? 'Untitled session';
|
|
4182
4174
|
preview.setAttribute('aria-current', String(historyPreview.state.target?.id === entry.id));
|
|
4183
|
-
} else title.textContent =
|
|
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.
|
|
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} ${
|
|
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 =
|
|
4226
|
-
const
|
|
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 &&
|
|
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 >
|
|
4246
|
-
|
|
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;
|
|
@@ -4619,6 +4607,7 @@ function loadGitHub() {
|
|
|
4619
4607
|
function setGitHub(github) {
|
|
4620
4608
|
const before = state.github?.signIn;
|
|
4621
4609
|
state.github = github;
|
|
4610
|
+
dropWatchAccounts(github.accounts);
|
|
4622
4611
|
const branches = githubPick.data.branches;
|
|
4623
4612
|
if (branches && !github.accounts.some((a) => a.id === branches.repo.accountId && !a.needsSignIn)) {
|
|
4624
4613
|
// Replace the generation even while the dock is closed; signing in again must
|
|
@@ -5304,6 +5293,7 @@ async function loadView(view) {
|
|
|
5304
5293
|
const request = { key, stamp, value: before?.stamp === stamp ? before.value : null, error: null, loading: true };
|
|
5305
5294
|
githubPick.data[view] = request;
|
|
5306
5295
|
renderGitHubViews();
|
|
5296
|
+
const watching = view === 'actions' ? new Set(runWatch.runs.values()) : null;
|
|
5307
5297
|
let value = null;
|
|
5308
5298
|
let error = null;
|
|
5309
5299
|
try {
|
|
@@ -5317,6 +5307,10 @@ async function loadView(view) {
|
|
|
5317
5307
|
githubPick.data[view] = { key, stamp, value: value ?? slot.value, error, loading: false };
|
|
5318
5308
|
renderGitHubViews();
|
|
5319
5309
|
if (view === 'actions') scheduleRuns();
|
|
5310
|
+
if (view === 'actions' && value) {
|
|
5311
|
+
runWatchSeen(repo, value.runs, watching);
|
|
5312
|
+
deliverRunWatch();
|
|
5313
|
+
}
|
|
5320
5314
|
}
|
|
5321
5315
|
|
|
5322
5316
|
// Each object is a listing generation, scoped to the selected account/repository.
|
|
@@ -5500,6 +5494,156 @@ function scheduleRuns() {
|
|
|
5500
5494
|
runsTimer = setTimeout(() => loadView('actions'), viewData('actions')?.value?.running ? RUNS_POLL_MS : RUNS_IDLE_POLL_MS);
|
|
5501
5495
|
}
|
|
5502
5496
|
|
|
5497
|
+
const RUN_WATCH_KEY = 'agentGuild.githubWatch';
|
|
5498
|
+
const RUN_FAILURES = new Set(['failure', 'timed_out', 'startup_failure', 'action_required']);
|
|
5499
|
+
const runWatch = { runs: new Map(), outcomes: [], failures: [], timer: 0, polling: null, told: null };
|
|
5500
|
+
|
|
5501
|
+
function runWatchKey(repo, id) {
|
|
5502
|
+
return `${repoKey(repo)}#${id}`;
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
function saveRunWatch() {
|
|
5506
|
+
const { runs, outcomes, failures } = runWatch;
|
|
5507
|
+
const value = runs.size || outcomes.length || failures.length ? JSON.stringify({ watches: [...runs.values()], outcomes, failures }) : null;
|
|
5508
|
+
try { value === null ? sessionStorage.removeItem(RUN_WATCH_KEY) : sessionStorage.setItem(RUN_WATCH_KEY, value); } catch {}
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
function loadRunWatch() {
|
|
5512
|
+
let saved = null;
|
|
5513
|
+
try { saved = JSON.parse(sessionStorage.getItem(RUN_WATCH_KEY) ?? 'null'); } catch { saved = null; }
|
|
5514
|
+
const list = (value) => (Array.isArray(value) ? value.filter((item) => item && typeof item === 'object') : []);
|
|
5515
|
+
runWatch.runs = new Map();
|
|
5516
|
+
for (const item of list(saved?.watches)) {
|
|
5517
|
+
if (Number.isSafeInteger(item.id) && Number.isSafeInteger(item.repo?.accountId) && typeof item.repo.fullName === 'string') runWatch.runs.set(runWatchKey(item.repo, item.id), item);
|
|
5518
|
+
}
|
|
5519
|
+
runWatch.outcomes = list(saved?.outcomes);
|
|
5520
|
+
runWatch.failures = list(saved?.failures);
|
|
5521
|
+
renderRunFailures();
|
|
5522
|
+
scheduleRunWatch();
|
|
5523
|
+
deliverRunWatch();
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
function watchedRun(repo, id) {
|
|
5527
|
+
return runWatch.runs.has(runWatchKey(repo, id));
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
function toggleRunWatch(repo, run) {
|
|
5531
|
+
const key = runWatchKey(repo, run.id);
|
|
5532
|
+
if (runWatch.runs.has(key)) runWatch.runs.delete(key);
|
|
5533
|
+
else {
|
|
5534
|
+
const { accountId, login, owner, name, fullName } = repo;
|
|
5535
|
+
runWatch.runs.set(key, { repo: { accountId, login, owner, name, fullName }, id: run.id, name: run.name, title: run.title, url: run.url });
|
|
5536
|
+
}
|
|
5537
|
+
saveRunWatch();
|
|
5538
|
+
renderGitHubViews();
|
|
5539
|
+
scheduleRunWatch();
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
function endWatch(key, outcome) {
|
|
5543
|
+
const watch = runWatch.runs.get(key);
|
|
5544
|
+
runWatch.runs.delete(key);
|
|
5545
|
+
runWatch.outcomes.push({ repo: watch.repo.fullName, name: watch.name, title: watch.title, url: watch.url, ...outcome });
|
|
5546
|
+
}
|
|
5547
|
+
|
|
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())) {
|
|
5550
|
+
const key = repoKey(repo);
|
|
5551
|
+
let changed = false;
|
|
5552
|
+
for (const [id, watch] of runWatch.runs) {
|
|
5553
|
+
if (repoKey(watch.repo) !== key || !watching.has(watch)) continue;
|
|
5554
|
+
const run = runs.find((r) => r.id === watch.id);
|
|
5555
|
+
if (run && run.status !== 'completed') continue;
|
|
5556
|
+
changed = true;
|
|
5557
|
+
if (!run) endWatch(id, { kind: 'ended', text: `${watch.title} is no longer among the latest runs of ${watch.repo.fullName}.` });
|
|
5558
|
+
else if (run.conclusion === 'success') endWatch(id, { kind: 'passed', text: `${watch.name} passed: ${watch.title}` });
|
|
5559
|
+
else if (RUN_FAILURES.has(run.conclusion)) endWatch(id, { kind: 'failed', label: runLabel(run), branch: run.branch });
|
|
5560
|
+
else endWatch(id, { kind: 'ended', text: `${watch.name} was ${runLabel(run)}: ${watch.title}` });
|
|
5561
|
+
}
|
|
5562
|
+
if (!changed) return;
|
|
5563
|
+
saveRunWatch();
|
|
5564
|
+
renderGitHubViews();
|
|
5565
|
+
scheduleRunWatch();
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
function dropWatchAccounts(accounts) {
|
|
5569
|
+
let changed = false;
|
|
5570
|
+
for (const [key, watch] of runWatch.runs) {
|
|
5571
|
+
if (accounts.some((a) => a.id === watch.repo.accountId)) continue;
|
|
5572
|
+
changed = true;
|
|
5573
|
+
endWatch(key, { kind: 'ended', text: `Stopped watching ${watch.title}: @${watch.repo.login} was signed out.` });
|
|
5574
|
+
}
|
|
5575
|
+
if (!changed) return;
|
|
5576
|
+
saveRunWatch();
|
|
5577
|
+
scheduleRunWatch();
|
|
5578
|
+
deliverRunWatch();
|
|
5579
|
+
}
|
|
5580
|
+
|
|
5581
|
+
function deliverRunWatch() {
|
|
5582
|
+
if (!runWatch.outcomes.length || runWatch.polling || document.visibilityState !== 'visible' || $('app').hidden) return;
|
|
5583
|
+
const outcomes = runWatch.outcomes;
|
|
5584
|
+
runWatch.outcomes = [];
|
|
5585
|
+
runWatch.failures.push(...outcomes.filter((o) => o.kind === 'failed'));
|
|
5586
|
+
saveRunWatch();
|
|
5587
|
+
renderRunFailures();
|
|
5588
|
+
const showing = runWatch.told && !$('toast').hidden && $('toast').contains(runWatch.told.node) ? runWatch.told.outcomes : [];
|
|
5589
|
+
const told = [...showing, ...outcomes.filter((o) => o.kind !== 'failed')];
|
|
5590
|
+
if (told.length) {
|
|
5591
|
+
const node = document.createTextNode(told.map((o) => o.text).join('\n'));
|
|
5592
|
+
runWatch.told = { node, outcomes: told };
|
|
5593
|
+
const single = told.length === 1 && told[0].url ? { label: 'GitHub', run: () => window.open(told[0].url, '_blank', 'noopener,noreferrer') } : null;
|
|
5594
|
+
toast(node, 8000, single);
|
|
5595
|
+
}
|
|
5596
|
+
if (outcomes.some((o) => o.kind === 'passed')) yardUi?.celebrate();
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
function renderRunFailures() {
|
|
5600
|
+
const failures = runWatch.failures;
|
|
5601
|
+
$('run-failed').hidden = !failures.length;
|
|
5602
|
+
if (!failures.length) return;
|
|
5603
|
+
$('run-failed-title').textContent = failures.length === 1 ? `${failures[0].name} ${failures[0].label}` : `${failures.length} watched runs failed`;
|
|
5604
|
+
$('run-failed-list').replaceChildren(...failures.map((o) => el('li', null, externalLink(o.title, o.url, 'run-failed-link'),
|
|
5605
|
+
` · ${[o.repo, o.branch, failures.length > 1 && `${o.name} ${o.label}`].filter(Boolean).join(' · ')}`)));
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
function dismissRunFailures() {
|
|
5609
|
+
runWatch.failures = [];
|
|
5610
|
+
saveRunWatch();
|
|
5611
|
+
renderRunFailures();
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
function scheduleRunWatch() {
|
|
5615
|
+
clearTimeout(runWatch.timer);
|
|
5616
|
+
runWatch.timer = 0;
|
|
5617
|
+
if (!runWatch.runs.size || runWatch.polling) return;
|
|
5618
|
+
runWatch.timer = setTimeout(pollRunWatch, document.visibilityState === 'visible' ? RUNS_POLL_MS : RUNS_IDLE_POLL_MS);
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
function pollRunWatch() {
|
|
5622
|
+
if (runWatch.polling) return runWatch.polling;
|
|
5623
|
+
clearTimeout(runWatch.timer);
|
|
5624
|
+
runWatch.timer = 0;
|
|
5625
|
+
runWatch.polling = (async () => {
|
|
5626
|
+
if (!state.connected || !runWatch.runs.size) return;
|
|
5627
|
+
if (!state.github) await api('GET', '/github').then(({ github }) => setGitHub(github), () => {});
|
|
5628
|
+
const watching = new Set(runWatch.runs.values());
|
|
5629
|
+
const repos = new Map();
|
|
5630
|
+
for (const watch of watching) repos.set(repoKey(watch.repo), watch.repo);
|
|
5631
|
+
const accounts = usableGitHubAccounts();
|
|
5632
|
+
await Promise.all([...repos.values()].filter((repo) => accounts.some((a) => a.id === repo.accountId)).map(async (repo) => {
|
|
5633
|
+
try {
|
|
5634
|
+
runWatchSeen(repo, (await api('GET', `${repoPath(repo)}/actions`)).runs, watching);
|
|
5635
|
+
} catch (err) {
|
|
5636
|
+
if (err instanceof AuthError) showAuth(err.message);
|
|
5637
|
+
}
|
|
5638
|
+
}));
|
|
5639
|
+
})().finally(() => {
|
|
5640
|
+
runWatch.polling = null;
|
|
5641
|
+
deliverRunWatch();
|
|
5642
|
+
scheduleRunWatch();
|
|
5643
|
+
});
|
|
5644
|
+
return runWatch.polling;
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5503
5647
|
function switchGitHubView(view, { focus = false } = {}) {
|
|
5504
5648
|
if (!GITHUB_VIEWS.includes(view)) return;
|
|
5505
5649
|
githubPick.view = view;
|
|
@@ -5963,7 +6107,15 @@ function renderRuns() {
|
|
|
5963
6107
|
const banner = slot?.value ? el('p', `github-running-note${slot.value.running ? ' on' : ''}`, slot.value.running ? 'A workflow is running. This list refreshes every few seconds.' : 'No workflow is running.') : null;
|
|
5964
6108
|
const rows = runs.map((run) => {
|
|
5965
6109
|
const meta = [run.name !== run.title && run.name, run.branch && `on ${run.branch}`, run.event, run.runNumber && `#${run.runNumber}`, run.updatedAt && relativeTime(run.updatedAt)].filter(Boolean).join(' · ');
|
|
5966
|
-
|
|
6110
|
+
const side = el('span', 'github-run-side', badge(runTone(run), runLabel(run), `Status: ${runLabel(run)}`));
|
|
6111
|
+
if (run.status !== 'completed') {
|
|
6112
|
+
const watching = watchedRun(githubPick.repo, run.id);
|
|
6113
|
+
const watch = keyed(button(watching ? 'Watching' : 'Watch', () => toggleRunWatch(githubPick.repo, run)), `watch:${run.id}`);
|
|
6114
|
+
watch.setAttribute('aria-pressed', String(watching));
|
|
6115
|
+
watch.title = watching ? 'Stop watching this run' : 'Fireworks when this run passes, a red screen when it fails';
|
|
6116
|
+
side.prepend(watch);
|
|
6117
|
+
}
|
|
6118
|
+
return recordRow(`run:${run.id}`, run.title, meta, side, run.url);
|
|
5967
6119
|
});
|
|
5968
6120
|
redraw(panel, [
|
|
5969
6121
|
viewTools('actions'),
|
|
@@ -7571,7 +7723,8 @@ function connectEvents() {
|
|
|
7571
7723
|
loadNews();
|
|
7572
7724
|
// A changelog.updated sent while the socket was down is lost; catch up the open panel.
|
|
7573
7725
|
if ($('changelog').open) loadChangelog();
|
|
7574
|
-
if (dockShows('github')) loadGitHub();
|
|
7726
|
+
if (dockShows('github') || runWatch.runs.size) loadGitHub();
|
|
7727
|
+
deliverRunWatch();
|
|
7575
7728
|
catchUpNotes(msg.notesRevision, msg.notesUnreadable === true);
|
|
7576
7729
|
loadProviders(ws).catch((err) => {
|
|
7577
7730
|
if (state.pageAway || state.remoteRevoked || state.eventsSocket !== ws) return;
|
|
@@ -7588,10 +7741,12 @@ function connectEvents() {
|
|
|
7588
7741
|
} else if (msg.type === 'yard-art.updated') {
|
|
7589
7742
|
state.yardArt = msg.yardArt;
|
|
7590
7743
|
notifyViews();
|
|
7744
|
+
} else if (msg.type === 'yard.fireworks') {
|
|
7745
|
+
yardUi?.celebrate();
|
|
7591
7746
|
} else if (msg.type === 'news.updated') {
|
|
7592
7747
|
loadNews();
|
|
7593
7748
|
} else if (msg.type === 'github.updated') {
|
|
7594
|
-
if (dockShows('github')) loadGitHub();
|
|
7749
|
+
if (dockShows('github') || runWatch.runs.size) loadGitHub();
|
|
7595
7750
|
} else if (msg.type === 'changelog.updated') {
|
|
7596
7751
|
if ($('changelog').open) loadChangelog();
|
|
7597
7752
|
} else if (msg.type === 'manager.upgrade') {
|
|
@@ -8425,5 +8580,12 @@ onSessionRequest((id) => {
|
|
|
8425
8580
|
if (state.connected && state.eventsSocket?.greeted) openAskedSession();
|
|
8426
8581
|
});
|
|
8427
8582
|
|
|
8583
|
+
document.addEventListener('visibilitychange', () => {
|
|
8584
|
+
if (document.visibilityState === 'visible' && runWatch.runs.size) pollRunWatch();
|
|
8585
|
+
else scheduleRunWatch();
|
|
8586
|
+
deliverRunWatch();
|
|
8587
|
+
});
|
|
8588
|
+
$('run-failed-dismiss').addEventListener('click', dismissRunFailures);
|
|
8589
|
+
loadRunWatch();
|
|
8428
8590
|
state.token = readTokenFromHash() || load(TOKEN_KEY);
|
|
8429
8591
|
boot();
|
package/web/history-preview.js
CHANGED
|
@@ -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 ? [...
|
|
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
|
|
29
|
-
if (err.code === 'history_changed') state
|
|
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">
|
|
@@ -776,6 +776,16 @@
|
|
|
776
776
|
|
|
777
777
|
<div id="tip" class="tip" aria-hidden="true" hidden><strong id="tip-title"></strong><span id="tip-source"></span><p id="tip-text"></p></div>
|
|
778
778
|
|
|
779
|
+
<div id="run-failed" class="run-failed" hidden>
|
|
780
|
+
<div class="run-failed-banner" role="alert">
|
|
781
|
+
<strong id="run-failed-title"></strong>
|
|
782
|
+
<ul id="run-failed-list" class="run-failed-list"></ul>
|
|
783
|
+
<div class="run-failed-actions">
|
|
784
|
+
<button id="run-failed-dismiss" class="btn primary" type="button">Dismiss</button>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
|
|
779
789
|
<div id="toast" class="toast" role="alert" popover="manual" hidden></div>
|
|
780
790
|
|
|
781
791
|
<template id="provider-template">
|
package/web/styles.css
CHANGED
|
@@ -1222,6 +1222,19 @@ a.btn.github-out::after {
|
|
|
1222
1222
|
.github-service-note .console-link { color: inherit; text-decoration: underline; }
|
|
1223
1223
|
.github-running-note.on { color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, var(--surface)); }
|
|
1224
1224
|
.badge.run { --badge: var(--warn); }
|
|
1225
|
+
.github-run-side { display: inline-flex; align-items: center; gap: 6px; }
|
|
1226
|
+
.run-failed { position: fixed; inset: 0; z-index: 13; pointer-events: none; background: color-mix(in srgb, var(--danger) 30%, transparent); }
|
|
1227
|
+
.run-failed:not([hidden]) { animation: fade-in 0.5s ease-out; }
|
|
1228
|
+
.run-failed-banner {
|
|
1229
|
+
pointer-events: auto; position: absolute; left: 50%; top: calc(var(--topbar-h, 56px) + 24px); transform: translateX(-50%);
|
|
1230
|
+
width: max-content; max-width: min(560px, calc(100vw - 32px)); max-height: calc(100dvh - var(--topbar-h, 56px) - 48px); padding: 14px 18px;
|
|
1231
|
+
display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 8px;
|
|
1232
|
+
background: var(--surface); color: var(--text); border: 2px solid var(--danger); border-radius: var(--radius); box-shadow: var(--shadow);
|
|
1233
|
+
}
|
|
1234
|
+
.run-failed-banner strong { color: var(--danger); font-size: 1.05rem; }
|
|
1235
|
+
.run-failed-list { margin: 0; padding: 0; list-style: none; display: grid; align-content: start; gap: 4px; color: var(--muted); overflow-wrap: anywhere; overflow-y: auto; }
|
|
1236
|
+
.run-failed-link { color: var(--text); }
|
|
1237
|
+
.run-failed-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
1225
1238
|
.badge.ok { --badge: var(--ok); }
|
|
1226
1239
|
.badge.bad { --badge: var(--danger); }
|
|
1227
1240
|
.badge.run, .badge.ok, .badge.bad { color: color-mix(in srgb, var(--badge) 72%, #000); border-color: color-mix(in srgb, var(--badge) 45%, transparent); }
|
|
@@ -1302,7 +1315,7 @@ body:has(#terminal-panel:not([hidden]) .terminal-controls:not([hidden])) .toast
|
|
|
1302
1315
|
}
|
|
1303
1316
|
|
|
1304
1317
|
@media (prefers-reduced-motion: reduce) {
|
|
1305
|
-
.status-pill.active::before, .agent.working::after, .model-detail .stat-fill, .toast:not([hidden]), .tip:not([hidden]), .menu:popover-open { animation: none; }
|
|
1318
|
+
.status-pill.active::before, .agent.working::after, .model-detail .stat-fill, .toast:not([hidden]), .tip:not([hidden]), .run-failed:not([hidden]), .menu:popover-open { animation: none; }
|
|
1306
1319
|
.model-name::before, .provider .copies summary::before, .menu-button::after, .btn, .videos-play img, .videos-play-icon, .models, .news-panel, .models::backdrop, .news-panel::backdrop { transition: none; }
|
|
1307
1320
|
.github-waiting::before, .github-running, body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after { animation: none; }
|
|
1308
1321
|
.btn:active:not(:disabled):not([aria-disabled="true"]) { transform: none; }
|