@oar-sh/oar 0.9.10 → 0.9.12
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/CHANGELOG.md +440 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +112 -25
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +217 -11
- package/server/claude-worker/claude-session-worker.mjs +26 -1
- package/server/claude-worker/task-output-discovery.mjs +126 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +68 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +46 -6
- package/server/grok-worker/grok-background-tasks.mjs +241 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +264 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/background-tasks-view.mjs +285 -16
- package/server/public/app/bootstrap.js +253 -37
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +567 -94
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +1033 -82
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +150 -10
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +138 -20
- package/server/public/app/store.js +121 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +37 -7
- package/server/public/app/task-console-render.mjs +237 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +630 -0
- package/server/public/app/task-output-text.mjs +234 -0
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
- package/server/public/app/transcript-jump-bottom.mjs +170 -22
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +159 -0
- package/server/public/index.html +625 -21
- package/server/relay-tools.md +3 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +9 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +433 -68
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +901 -253
- package/server/server-runtime.mjs +392 -99
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +365 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/relay-prompt-sanitizer.mjs +11 -4
- package/server/services/remote-relay-dispatcher.mjs +187 -7
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +231 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +562 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +35 -3
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/task-output-text.mjs +737 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// The About OAR modal, as HTML from a GET /api/about payload. No DOM here;
|
|
2
|
+
// about-ui.js opens the modal and wires it.
|
|
3
|
+
|
|
4
|
+
export const OAR_REPO_URL = 'https://github.com/oar-sh/oar';
|
|
5
|
+
export const OAR_SITE_URL = 'https://oar.sh';
|
|
6
|
+
export const OAR_RELEASES_URL = 'https://github.com/oar-sh/oar/releases';
|
|
7
|
+
|
|
8
|
+
const STATE_LABELS = Object.freeze({
|
|
9
|
+
online: 'online',
|
|
10
|
+
offline: 'offline',
|
|
11
|
+
idle: 'idle',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const USAGE_LABELS = Object.freeze({
|
|
15
|
+
github: 'GitHub Copilot',
|
|
16
|
+
cursor: 'Cursor',
|
|
17
|
+
grok: 'Grok',
|
|
18
|
+
'claude-cloud': 'Claude Cloud',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function esc(value) {
|
|
22
|
+
return String(value ?? '')
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
.replace(/'/g, ''');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function finite(value) {
|
|
31
|
+
const number = Number(value);
|
|
32
|
+
return Number.isFinite(number) ? number : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function aboutMenuLabel(version) {
|
|
36
|
+
const text = String(version || '').trim();
|
|
37
|
+
return text && text !== '0.0.0' ? `ℹ️ About OAR · ${text}` : 'ℹ️ About OAR';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatCount(value) {
|
|
41
|
+
const number = finite(value);
|
|
42
|
+
return number === null ? '–' : Math.round(number).toLocaleString('en-US');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Tokens and other large amounts: 1 234, 12.3 k, 4.56 M. */
|
|
46
|
+
export function formatAmount(value) {
|
|
47
|
+
const number = finite(value);
|
|
48
|
+
if (number === null) return '–';
|
|
49
|
+
if (Math.abs(number) >= 1e9) return `${(number / 1e9).toFixed(2)} B`;
|
|
50
|
+
if (Math.abs(number) >= 1e6) return `${(number / 1e6).toFixed(2)} M`;
|
|
51
|
+
if (Math.abs(number) >= 1e4) return `${(number / 1e3).toFixed(1)} k`;
|
|
52
|
+
return Number.isInteger(number) ? number.toLocaleString('en-US') : number.toFixed(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatUsd(value) {
|
|
56
|
+
const number = finite(value);
|
|
57
|
+
return number === null ? '–' : `USD ${number.toFixed(2)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatBytes(value) {
|
|
61
|
+
const number = finite(value);
|
|
62
|
+
if (number === null || number < 0) return '–';
|
|
63
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
64
|
+
let size = number;
|
|
65
|
+
let unit = 0;
|
|
66
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
67
|
+
size /= 1024;
|
|
68
|
+
unit += 1;
|
|
69
|
+
}
|
|
70
|
+
return unit === 0 ? `${Math.round(size)} B` : `${size.toFixed(size >= 100 ? 0 : 1)} ${units[unit]}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatUptime(seconds) {
|
|
74
|
+
const total = finite(seconds);
|
|
75
|
+
if (total === null || total < 0) return '–';
|
|
76
|
+
const days = Math.floor(total / 86_400);
|
|
77
|
+
const hours = Math.floor((total % 86_400) / 3_600);
|
|
78
|
+
const minutes = Math.floor((total % 3_600) / 60);
|
|
79
|
+
if (days > 0) return `${days} d ${hours} h`;
|
|
80
|
+
if (hours > 0) return `${hours} h ${minutes} min`;
|
|
81
|
+
if (minutes > 0) return `${minutes} min`;
|
|
82
|
+
return `${Math.floor(total)} s`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function defaultFormatDateTime(iso) {
|
|
86
|
+
const at = Date.parse(iso);
|
|
87
|
+
if (!Number.isFinite(at)) return '–';
|
|
88
|
+
return new Date(at).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function defaultFormatDate(iso) {
|
|
92
|
+
const at = Date.parse(iso);
|
|
93
|
+
if (!Number.isFinite(at)) return '–';
|
|
94
|
+
return new Date(at).toLocaleDateString(undefined, { dateStyle: 'medium' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function row(label, value, { hint = '', title = '' } = {}) {
|
|
98
|
+
return `<tr><td><div class="plan-usage-detail-label"><span>${esc(label)}</span>${hint ? `<span class="plan-usage-detail-hint">${esc(hint)}</span>` : ''}</div></td>`
|
|
99
|
+
+ `<td class="plan-usage-detail-value"${title ? ` title="${esc(title)}"` : ''}>${value}</td></tr>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function section(id, title, inner, { subtitle = '' } = {}) {
|
|
103
|
+
return `<section class="plan-usage-card about-section" data-about-section="${esc(id)}">`
|
|
104
|
+
+ `<div class="plan-usage-card-head"><div class="plan-usage-card-titles"><span class="plan-usage-card-title">${esc(title)}</span>`
|
|
105
|
+
+ `${subtitle ? `<span class="plan-usage-account">${esc(subtitle)}</span>` : ''}</div></div>${inner}</section>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function table(rows) {
|
|
109
|
+
return rows.length ? `<table class="plan-usage-detail-table"><tbody>${rows.join('')}</tbody></table>` : '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function note(text) {
|
|
113
|
+
return `<div class="plan-usage-detail-note">${text}</div>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function externalLink(href, text) {
|
|
117
|
+
return `<a class="plan-usage-link" href="${esc(href)}" target="_blank" rel="noopener">${esc(text)}</a>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function providerDetails(provider) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (provider.engine === 'extension') parts.push('extension engine');
|
|
123
|
+
if (provider.engine === 'sdk') parts.push('SDK engine');
|
|
124
|
+
if (provider.cliVersion) parts.push(`CLI ${provider.cliVersion}`);
|
|
125
|
+
if (provider.sdkVersion) parts.push(`SDK ${provider.sdkVersion}`);
|
|
126
|
+
return parts.join(' · ');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function providerState(provider) {
|
|
130
|
+
const state = STATE_LABELS[provider.state] ? provider.state : 'idle';
|
|
131
|
+
const workers = finite(provider.runningWorkers) || 0;
|
|
132
|
+
const count = workers > 0 ? ` · ${workers} ${workers === 1 ? 'worker' : 'workers'}` : '';
|
|
133
|
+
return `<span class="about-state" data-state="${esc(state)}">${esc(STATE_LABELS[state])}</span>${esc(count)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function usageRows(id, usage) {
|
|
137
|
+
if (id === 'github') {
|
|
138
|
+
return [row('Premium requests', esc(formatAmount(usage.premiumRequests)), { hint: `over ${formatCount(usage.turns)} replies` })];
|
|
139
|
+
}
|
|
140
|
+
if (id === 'cursor') {
|
|
141
|
+
return [
|
|
142
|
+
row('Tokens', esc(formatAmount(usage.totalTokens)), {
|
|
143
|
+
hint: `${formatAmount(usage.inputTokens)} in · ${formatAmount(usage.outputTokens)} out · ${formatAmount((finite(usage.cacheReadTokens) || 0) + (finite(usage.cacheWriteTokens) || 0))} cache`,
|
|
144
|
+
}),
|
|
145
|
+
row('Cost', esc(formatUsd(usage.rawCostUsd)), { hint: `model price over ${formatCount(usage.runs)} runs; charged ${formatUsd(usage.chargedUsd)}` }),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
if (id === 'grok') {
|
|
149
|
+
return [
|
|
150
|
+
row('Tokens', esc(formatAmount(usage.totalTokens)), { hint: `${formatAmount(usage.inputTokens)} in · ${formatAmount(usage.outputTokens)} out` }),
|
|
151
|
+
row('Cost', esc(formatUsd(usage.costUsd)), { hint: `over ${formatCount(usage.turns)} turns` }),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
if (id === 'claude-cloud') {
|
|
155
|
+
return [row('Cost', esc(formatUsd(usage.costUsd)), { hint: `over ${formatCount(usage.sessions)} cloud sessions` })];
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildUpdateSection(update, formatDateTime) {
|
|
161
|
+
if (update.checkEnabled) {
|
|
162
|
+
if (update.available && update.latestVersion) {
|
|
163
|
+
return section('update', 'Updates', note(`Latest: ${esc(update.latestVersion)} · ${externalLink(update.notesUrl || OAR_RELEASES_URL, 'release notes')}`));
|
|
164
|
+
}
|
|
165
|
+
const checked = update.lastCheckedAt ? `, checked ${esc(formatDateTime(update.lastCheckedAt))}` : '';
|
|
166
|
+
return section('update', 'Updates', note(update.lastCheckedAt ? `Up to date${checked}.` : 'Not checked yet.'));
|
|
167
|
+
}
|
|
168
|
+
if (update.checkKilled) {
|
|
169
|
+
return section('update', 'Updates', note('Update checks are turned off on this relay (OAR_NO_UPDATE_CHECK).'));
|
|
170
|
+
}
|
|
171
|
+
return section('update', 'Updates', note(
|
|
172
|
+
'Update checks are off. Turn on <button type="button" class="plan-usage-link about-inline-button" data-about-open-settings>Settings › General › Check for updates automatically</button> to see the latest version here.',
|
|
173
|
+
));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {object} about the GET /api/about payload
|
|
178
|
+
* @param {object} [options]
|
|
179
|
+
* @param {(iso: string) => string} [options.formatDateTime]
|
|
180
|
+
* @param {(iso: string) => string} [options.formatDate]
|
|
181
|
+
*/
|
|
182
|
+
export function buildAboutModalHtml(about = {}, {
|
|
183
|
+
formatDateTime = defaultFormatDateTime,
|
|
184
|
+
formatDate = defaultFormatDate,
|
|
185
|
+
} = {}) {
|
|
186
|
+
const sections = [];
|
|
187
|
+
|
|
188
|
+
const commit = String(about.commit || '').trim();
|
|
189
|
+
const install = about.installMethod === 'git-checkout' ? 'git checkout' : (about.installMethod === 'npm-global' ? 'npm package' : '');
|
|
190
|
+
sections.push(section('build', 'OAR', table([
|
|
191
|
+
row('Version', esc(about.version || '–'), install ? { hint: install } : {}),
|
|
192
|
+
...(commit ? [row('Commit', `<code>${esc(commit.slice(0, 7))}</code>`, { title: commit })] : []),
|
|
193
|
+
row('Started', esc(about.startedAt ? formatDateTime(about.startedAt) : '–')),
|
|
194
|
+
row('Uptime', esc(formatUptime(about.uptimeSeconds))),
|
|
195
|
+
])));
|
|
196
|
+
|
|
197
|
+
const providers = Array.isArray(about.providers) ? about.providers : [];
|
|
198
|
+
sections.push(section('providers', 'Providers', providers.length
|
|
199
|
+
? table(providers.map((provider) => row(provider.label || provider.id, providerState(provider), { hint: providerDetails(provider) })))
|
|
200
|
+
+ (providers.some((provider) => provider.state === 'idle')
|
|
201
|
+
? note('Idle: on, with no worker running right now; one starts with the next turn.')
|
|
202
|
+
: '')
|
|
203
|
+
: note('No provider is turned on.')));
|
|
204
|
+
|
|
205
|
+
const stats = about.stats || {};
|
|
206
|
+
const turnsByProvider = Array.isArray(stats.turnsByProvider) ? stats.turnsByProvider : [];
|
|
207
|
+
const archived = finite(stats.archivedConversations) || 0;
|
|
208
|
+
sections.push(section('activity', 'Activity', table([
|
|
209
|
+
row('Conversations', esc(formatCount(stats.conversations)), archived ? { hint: `${formatCount(archived)} archived` } : {}),
|
|
210
|
+
row('Turns', esc(formatCount(stats.turns)), { hint: 'replies written by an agent' }),
|
|
211
|
+
row('Messages', esc(formatCount(stats.messages))),
|
|
212
|
+
...turnsByProvider.map((entry) => row(`Turns on ${entry.label || entry.id}`, esc(formatCount(entry.turns)))),
|
|
213
|
+
]), { subtitle: stats.firstConversationAt ? `since ${formatDate(stats.firstConversationAt)}` : '' }));
|
|
214
|
+
|
|
215
|
+
const usage = about.usage && typeof about.usage === 'object' ? about.usage : {};
|
|
216
|
+
const usageIds = Object.keys(USAGE_LABELS).filter((id) => usage[id]);
|
|
217
|
+
sections.push(section('usage', 'Usage', (usageIds.length
|
|
218
|
+
? usageIds.map((id) => `<div class="about-usage-provider">${esc(USAGE_LABELS[id])}</div>${table(usageRows(id, usage[id]))}`).join('')
|
|
219
|
+
: note('No provider has reported usage yet.'))
|
|
220
|
+
+ note('What the providers report and this relay keeps: Copilot premium requests per reply, Cursor and Grok tokens and cost per billing cycle, Claude Cloud cost per session. Claude Code and OpenAI turns keep no usage here.')));
|
|
221
|
+
|
|
222
|
+
const relay = about.relay || {};
|
|
223
|
+
sections.push(section('relay', 'Relay', table([
|
|
224
|
+
row('Uptime', esc(formatUptime(relay.uptimeSeconds))),
|
|
225
|
+
row('Database', esc(formatBytes(relay.dbBytes)), { hint: `${formatBytes(relay.dbFileBytes)} + ${formatBytes(relay.walBytes)} WAL` }),
|
|
226
|
+
row('Running workers', esc(formatCount(relay.runningWorkers))),
|
|
227
|
+
])));
|
|
228
|
+
|
|
229
|
+
sections.push(buildUpdateSection(about.update || {}, formatDateTime));
|
|
230
|
+
|
|
231
|
+
sections.push(`<div class="about-links" data-about-section="links">${externalLink(OAR_REPO_URL, 'github.com/oar-sh/oar')}${externalLink(OAR_SITE_URL, 'oar.sh')}</div>`);
|
|
232
|
+
|
|
233
|
+
return `<div class="about-modal">${sections.join('')}</div>`;
|
|
234
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
refreshWorkspaceRootHints,
|
|
20
20
|
} from './api-client.js';
|
|
21
21
|
import { isSuspendHostActionVisible } from './settings-modal.js';
|
|
22
|
+
import { describeRelayRestartError } from './relay-restart-error.mjs';
|
|
22
23
|
import {
|
|
23
24
|
applyHostSuspendState,
|
|
24
25
|
describeHostSuspendBlocker,
|
|
@@ -311,10 +312,17 @@ export async function confirmEmptyQueue() {
|
|
|
311
312
|
emptyQueueInFlight = true;
|
|
312
313
|
setSummaryModalLoading(true);
|
|
313
314
|
try {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
let result = null;
|
|
316
|
+
try {
|
|
317
|
+
result = await requestQueueEmpty({
|
|
318
|
+
reason: 'manual-empty-queue',
|
|
319
|
+
requestedBy: 'localhost-api',
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
closeSummaryModal();
|
|
323
|
+
alert(`Failed to empty queue: ${error?.message || 'unknown error'}`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
318
326
|
closeSummaryModal();
|
|
319
327
|
if (!result?.ok) {
|
|
320
328
|
alert('Failed to empty queue');
|
|
@@ -334,23 +342,32 @@ export async function confirmEmptyQueue() {
|
|
|
334
342
|
}
|
|
335
343
|
}
|
|
336
344
|
|
|
345
|
+
/** A failed restart request: a refusal as an alert, no answer as a notice. */
|
|
346
|
+
export function showRelayRestartRequestError(error) {
|
|
347
|
+
const { kind, text } = describeRelayRestartError(error);
|
|
348
|
+
if (kind === 'refused') alert(text);
|
|
349
|
+
else showTransientRelayNotice(text, 9000);
|
|
350
|
+
}
|
|
351
|
+
|
|
337
352
|
export async function confirmRestartWebRelay() {
|
|
338
353
|
if (restartRelayInFlight) return;
|
|
339
354
|
restartRelayInFlight = true;
|
|
340
355
|
setSummaryModalLoading(true);
|
|
341
356
|
try {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
let result = null;
|
|
358
|
+
try {
|
|
359
|
+
result = await requestRelayRestart({
|
|
360
|
+
reason: 'manual-restart',
|
|
361
|
+
requestedBy: 'localhost-api',
|
|
362
|
+
restart: true,
|
|
363
|
+
});
|
|
364
|
+
} catch (error) {
|
|
365
|
+
closeSummaryModal();
|
|
366
|
+
showRelayRestartRequestError(error);
|
|
351
367
|
return;
|
|
352
368
|
}
|
|
353
|
-
|
|
369
|
+
closeSummaryModal();
|
|
370
|
+
if (!result?.ok) {
|
|
354
371
|
alert('Failed to queue relay restart');
|
|
355
372
|
return;
|
|
356
373
|
}
|
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
// Agent-to-agent exchanges in the transcript: a prompt another agent sent this
|
|
2
|
-
// session (another relay's agent, or an agent on this relay)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// session (another relay's agent, or an agent on this relay) folds into ONE
|
|
3
|
+
// row, "note from win-test · report builder · answered below", which opens to
|
|
4
|
+
// the prompt as it renders otherwise.
|
|
5
5
|
//
|
|
6
6
|
// DOM-free on purpose, like relay-event-view.mjs: conversation-view.js reads
|
|
7
7
|
// the rows off the transcript, plans with this module and applies the plan.
|
|
8
8
|
// Presentation only: the transcript data is never touched.
|
|
9
9
|
//
|
|
10
|
-
// Nothing addressed to the user is folded away.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// caller keeps it shown while the page lives instead of folding it away under
|
|
21
|
-
// the user once the card is answered.
|
|
10
|
+
// Nothing addressed to the user is folded away. The fold covers the agent's
|
|
11
|
+
// prompt and nothing else: this session's reply to it can carry a line for
|
|
12
|
+
// the user, so the reply, the live bubble of the running turn and every card
|
|
13
|
+
// of the turn always show, as they do for a message of the user's own. The
|
|
14
|
+
// row still says how the turn stands.
|
|
15
|
+
//
|
|
16
|
+
// One prompt is left as it is: an agent's prompt that was steered into a
|
|
17
|
+
// running turn. It renders as part of that turn's flow (steered-turn-merge.mjs:
|
|
18
|
+
// the dashed bubble, the marker attached under it), and a row in its place
|
|
19
|
+
// would break that flow.
|
|
22
20
|
|
|
23
21
|
import { isRelayEventMessage } from './relay-event-view.mjs';
|
|
24
22
|
import { relayErrorCodeFromText } from './relay-error-ctas.mjs';
|
|
25
23
|
import { isLocalAgentOrigin, localAgentOriginModel, normalizeRemoteOrigin } from './remote-relay-origin-view.mjs';
|
|
26
24
|
|
|
27
25
|
export const AGENT_EXCHANGE_HEAD_CLASS = 'agent-exchange-head';
|
|
28
|
-
export const AGENT_EXCHANGE_MEMBER_CLASS = 'agent-exchange-member';
|
|
29
26
|
export const AGENT_EXCHANGE_CLOSED_CLASS = 'agent-exchange-closed';
|
|
30
27
|
|
|
31
28
|
export const AGENT_EXCHANGE_STATUS_TEXT = Object.freeze({
|
|
32
29
|
working: 'working…',
|
|
33
|
-
replied: '
|
|
30
|
+
replied: 'answered below',
|
|
34
31
|
failed: 'failed',
|
|
35
32
|
stopped: 'stopped',
|
|
36
33
|
none: 'no reply yet',
|
|
@@ -41,9 +38,6 @@ const STOPPED_ERROR_CODE = 'turn-aborted';
|
|
|
41
38
|
// A steer's settle markers (steered-turn-merge.mjs): the message was consumed
|
|
42
39
|
// by another turn, folded into it or stopped with it.
|
|
43
40
|
const STEER_MARKER_KINDS = new Set(['folded', 'stopped']);
|
|
44
|
-
const ABSORBED_KIND = 'absorbed';
|
|
45
|
-
const ANSWERED_ELSEWHERE_KIND = 'answered-elsewhere';
|
|
46
|
-
const TRANSIENT_REASONS = new Set(['open-card', 'user-waiting']);
|
|
47
41
|
|
|
48
42
|
function defaultEscape(value) {
|
|
49
43
|
return String(value ?? '')
|
|
@@ -58,10 +52,6 @@ function oneLine(value) {
|
|
|
58
52
|
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
function idSet(values) {
|
|
62
|
-
return new Set(Array.from(values || [], (value) => String(value ?? '').trim()).filter(Boolean));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
55
|
/** True for an `origin` that names an agent: one on this relay or another relay's. */
|
|
66
56
|
export function isAgentOrigin(origin) {
|
|
67
57
|
return isLocalAgentOrigin(origin) || !!normalizeRemoteOrigin(origin);
|
|
@@ -87,12 +77,10 @@ export function replyOutcome(text) {
|
|
|
87
77
|
/**
|
|
88
78
|
* What the plan needs of one message, built when its row is: an agent
|
|
89
79
|
* prompt's origin, an assistant row's kind, the prompt it answers and how it
|
|
90
|
-
* ended.
|
|
91
|
-
* tolerant parser; Date.parse by default).
|
|
80
|
+
* ended.
|
|
92
81
|
*/
|
|
93
|
-
export function agentExchangeRowOf(message, { id = message?.id
|
|
82
|
+
export function agentExchangeRowOf(message, { id = message?.id } = {}) {
|
|
94
83
|
const role = message?.role === 'assistant' ? 'assistant' : (message?.role === 'user' ? 'user' : '');
|
|
95
|
-
const timestampMs = Number(parseTimestamp(message?.timestamp));
|
|
96
84
|
return {
|
|
97
85
|
id: String(id ?? '').trim(),
|
|
98
86
|
role,
|
|
@@ -100,15 +88,10 @@ export function agentExchangeRowOf(message, { id = message?.id, parseTimestamp =
|
|
|
100
88
|
origin: isAgentPromptMessage(message) ? message.origin : null,
|
|
101
89
|
kind: String(message?.kind || '').trim(),
|
|
102
90
|
sourceMessageId: role === 'assistant' ? String(message?.sourceMessageId || '').trim() : '',
|
|
103
|
-
timestampMs: Number.isFinite(timestampMs) && timestampMs > 0 ? timestampMs : 0,
|
|
104
91
|
outcome: role === 'assistant' ? replyOutcome(message?.text) : '',
|
|
105
92
|
};
|
|
106
93
|
}
|
|
107
94
|
|
|
108
|
-
function isUsersOwnMessage(row) {
|
|
109
|
-
return row.role === 'user' && !row.origin && !row.relayEvent;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
95
|
/**
|
|
113
96
|
* The plan for a transcript: one entry per agent prompt, in transcript order.
|
|
114
97
|
*
|
|
@@ -116,96 +99,52 @@ function isUsersOwnMessage(row) {
|
|
|
116
99
|
* `steered` for a user row steered-turn-merge.mjs classed as steered), and
|
|
117
100
|
* the live bubble of a running turn as `{ id: <its message id>, role:
|
|
118
101
|
* 'assistant', live: true }`. `runningIds` names turns that run without a
|
|
119
|
-
* live bubble on the page
|
|
120
|
-
* approval card, `keptShownIds` those the caller keeps shown.
|
|
102
|
+
* live bubble on the page.
|
|
121
103
|
*
|
|
122
|
-
* Each entry: `{ promptId, prompt,
|
|
123
|
-
*
|
|
124
|
-
*
|
|
104
|
+
* Each entry: `{ promptId, prompt, status, fold }`. The prompt's row is the
|
|
105
|
+
* only one a fold covers; the replies and the live bubble are read for the
|
|
106
|
+
* status alone. `fold` is false for a prompt steered into a running turn.
|
|
125
107
|
*/
|
|
126
|
-
export function planAgentExchangeFolds(rows, { runningIds = []
|
|
108
|
+
export function planAgentExchangeFolds(rows, { runningIds = [] } = {}) {
|
|
127
109
|
const list = (Array.isArray(rows) ? rows : []).filter((row) => row && String(row.id || '').trim());
|
|
128
|
-
const running =
|
|
129
|
-
const openCards = idSet(openCardIds);
|
|
130
|
-
const keptShown = idSet(keptShownIds);
|
|
110
|
+
const running = new Set(Array.from(runningIds || [], (value) => String(value ?? '').trim()).filter(Boolean));
|
|
131
111
|
|
|
132
|
-
// One turn per user-side row (the user's, an agent's, a relay event), with
|
|
133
|
-
// its replies and its live bubble.
|
|
134
112
|
const turns = [];
|
|
135
113
|
const turnById = new Map();
|
|
136
|
-
|
|
137
|
-
if (row.role !== 'user' || turnById.has(row.id))
|
|
138
|
-
const turn = { prompt: row,
|
|
114
|
+
for (const row of list) {
|
|
115
|
+
if (row.role !== 'user' || !row.origin || turnById.has(row.id)) continue;
|
|
116
|
+
const turn = { prompt: row, replies: [], live: false };
|
|
139
117
|
turns.push(turn);
|
|
140
118
|
turnById.set(row.id, turn);
|
|
141
|
-
}
|
|
119
|
+
}
|
|
142
120
|
for (const row of list) {
|
|
143
121
|
if (row.role !== 'assistant') continue;
|
|
144
122
|
const turn = turnById.get(row.live ? row.id : row.sourceMessageId);
|
|
145
123
|
if (!turn) continue;
|
|
146
|
-
if (row.live) turn.live =
|
|
124
|
+
if (row.live) turn.live = true;
|
|
147
125
|
else turn.replies.push(row);
|
|
148
126
|
}
|
|
149
|
-
for (const turn of turns) {
|
|
150
|
-
turn.running = !!turn.live || running.has(turn.prompt.id);
|
|
151
|
-
turn.markers = turn.replies.filter((row) => STEER_MARKER_KINDS.has(row.kind));
|
|
152
|
-
turn.answers = turn.replies.filter((row) => !STEER_MARKER_KINDS.has(row.kind) && row.kind !== ANSWERED_ELSEWHERE_KIND);
|
|
153
|
-
// When the turn ended (its last answer was written); still open while it
|
|
154
|
-
// runs; null for a turn that has not run (queued, cancelled).
|
|
155
|
-
turn.end = turn.answers.length
|
|
156
|
-
? Math.max(0, ...turn.answers.map((row) => Number(row.timestampMs) || 0))
|
|
157
|
-
: (turn.running ? Infinity : null);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// A steer folded into a turn (or stopped with it) settles as a marker under
|
|
161
|
-
// its own message; the turn it joined is the one running when it was sent:
|
|
162
|
-
// sent before it, ended after it, and of those the first to end (a
|
|
163
|
-
// session runs one turn at a time).
|
|
164
|
-
const steeredInto = new Set();
|
|
165
|
-
for (const turn of turns) {
|
|
166
|
-
const sentAt = Number(turn.prompt.timestampMs) || 0;
|
|
167
|
-
if (!turn.markers.length || !sentAt) continue;
|
|
168
|
-
let host = null;
|
|
169
|
-
for (const other of turns) {
|
|
170
|
-
const startedAt = Number(other.prompt.timestampMs) || 0;
|
|
171
|
-
if (other === turn || other.end === null || !startedAt) continue;
|
|
172
|
-
if (startedAt > sentAt || other.end < sentAt) continue;
|
|
173
|
-
if (!host || other.end < host.end || (other.end === host.end && startedAt > host.prompt.timestampMs)) host = other;
|
|
174
|
-
}
|
|
175
|
-
if (host) steeredInto.add(host);
|
|
176
|
-
}
|
|
177
127
|
|
|
178
|
-
return turns.
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
if (turn.
|
|
187
|
-
&& isUsersOwnMessage(other.prompt)
|
|
188
|
-
&& !other.replies.length
|
|
189
|
-
&& !other.running)) {
|
|
190
|
-
reasons.push('user-waiting');
|
|
191
|
-
}
|
|
192
|
-
if (keptShown.has(turn.prompt.id)) reasons.push('kept-shown');
|
|
193
|
-
const last = turn.answers[turn.answers.length - 1] || null;
|
|
194
|
-
const status = last ? (last.outcome || 'replied') : (turn.running ? 'working' : 'none');
|
|
128
|
+
return turns.map((turn) => {
|
|
129
|
+
const markers = turn.replies.filter((row) => STEER_MARKER_KINDS.has(row.kind));
|
|
130
|
+
// An answer, or the marker that the answer is on a continuation row
|
|
131
|
+
// further down.
|
|
132
|
+
const answers = turn.replies.filter((row) => !STEER_MARKER_KINDS.has(row.kind));
|
|
133
|
+
const last = answers[answers.length - 1] || null;
|
|
134
|
+
let status = 'none';
|
|
135
|
+
if (last) status = last.outcome || 'replied';
|
|
136
|
+
else if (turn.live || running.has(turn.prompt.id)) status = 'working';
|
|
195
137
|
return {
|
|
196
138
|
promptId: turn.prompt.id,
|
|
197
139
|
prompt: turn.prompt,
|
|
198
|
-
members: turn.live ? [...turn.replies, turn.live] : turn.replies.slice(),
|
|
199
140
|
status,
|
|
200
|
-
fold:
|
|
201
|
-
reasons,
|
|
202
|
-
transient: reasons.some((reason) => TRANSIENT_REASONS.has(reason)),
|
|
141
|
+
fold: !turn.prompt.steered && !markers.length,
|
|
203
142
|
};
|
|
204
143
|
});
|
|
205
144
|
}
|
|
206
145
|
|
|
207
146
|
/**
|
|
208
|
-
* Whether a folded
|
|
147
|
+
* Whether a folded prompt shows open. `choice` is the user's own toggle of
|
|
209
148
|
* it (true opened, false closed, undefined none) and always wins; without
|
|
210
149
|
* one it is open while the turn works and closes by itself once it is
|
|
211
150
|
* replied, failed or stopped.
|
|
@@ -234,8 +173,8 @@ export function agentExchangeSummary(origin, status, { conversations = null } =
|
|
|
234
173
|
|
|
235
174
|
/**
|
|
236
175
|
* The folded row's markup: a <details> whose summary is the whole row. Its
|
|
237
|
-
* body stays empty — the prompt
|
|
238
|
-
*
|
|
176
|
+
* body stays empty — the prompt's bubble is its sibling in the prompt's own
|
|
177
|
+
* node, shown while it is open.
|
|
239
178
|
*/
|
|
240
179
|
export function renderAgentExchangeRowHtml(summary, { escHtml = defaultEscape, timeText = '', open = false } = {}) {
|
|
241
180
|
return `<details class="agent-exchange-row agent-exchange-${escHtml(summary?.status || 'none')}"${open ? ' open' : ''}>`
|