@oar-sh/oar 0.9.23 → 0.9.25
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/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
- package/CHANGELOG.md +307 -0
- package/README.md +12 -11
- package/bin/oar.js +54 -4
- package/package.json +11 -7
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +20 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +13 -8
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
- package/server/migrations/0039-media-index.mjs +167 -0
- package/server/migrations/0040-image-session-defaults.mjs +93 -0
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +149 -8
- package/server/public/app/ask-user-view.js +2 -1
- package/server/public/app/attachments-view.js +66 -9
- package/server/public/app/bootstrap.js +289 -94
- package/server/public/app/chat-actions-menu-state.mjs +1 -0
- package/server/public/app/context-media-tab.js +143 -0
- package/server/public/app/conversation-view.js +90 -5
- package/server/public/app/image-composer-ui.js +518 -0
- package/server/public/app/image-turn-placeholder.mjs +233 -0
- package/server/public/app/journal-view.js +12 -2
- package/server/public/app/media-gallery-model.mjs +202 -0
- package/server/public/app/media-gallery-ui.js +524 -0
- package/server/public/app/media-view.js +79 -0
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/openai-image-composer.mjs +492 -0
- package/server/public/app/openai-image-cost-ui.js +426 -0
- package/server/public/app/openai-image-spend.mjs +443 -0
- package/server/public/app/plan-usage-view.mjs +38 -6
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/relay-error-ctas.mjs +5 -0
- package/server/public/app/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +171 -0
- package/server/public/app/socket-handlers.js +47 -0
- package/server/public/app/tab-strip-fade.mjs +1 -1
- package/server/public/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +478 -9
- package/server/relay-tools.md +1 -0
- package/server/repositories/image-conversation-repository.mjs +69 -0
- package/server/repositories/media-repository.mjs +305 -0
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/media-routes.mjs +361 -0
- package/server/routes/messages-routes.mjs +1467 -343
- package/server/routes/sessions-routes.mjs +596 -123
- package/server/server-runtime.mjs +414 -81
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +23 -5
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +4 -2
- package/server/services/generated-image-cleanup-service.mjs +2 -1
- package/server/services/generated-image-files.mjs +110 -0
- package/server/services/image-input-paths.mjs +389 -0
- package/server/services/image-operation-service.mjs +6 -2
- package/server/services/media-extract.mjs +653 -0
- package/server/services/media-index-service.mjs +221 -0
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/oar-cli-helpers.mjs +72 -9
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/openai-image-preview-turns.mjs +330 -0
- package/server/services/openai-image-spend-service.mjs +516 -0
- package/server/services/openai-image-stream.mjs +170 -0
- package/server/services/plan-usage-openai-image.mjs +148 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +386 -22
- package/server/services/remote-relay-registry-service.mjs +3 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/token-usage-service.mjs +15 -0
- package/server/services/update-install-service.mjs +2 -2
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/image-header.mjs +90 -0
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/openai-image-options.mjs +299 -0
- package/shared/openai-image-presets.mjs +175 -0
- package/shared/openai-image-previews.mjs +40 -0
- package/shared/openai-image-prices.mjs +291 -0
- package/shared/openai-image-spend.mjs +90 -0
- package/shared/remote-relay-contract.mjs +126 -4
- package/shared/worker-runtime/direct-openai-image.mjs +98 -48
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Agent SDK rows inside Settings: the Claude Agent SDK in the Claude, Claude
|
|
2
|
+
// API and DeepSeek tabs, the Cursor SDK in the Cursor tab, and the Windows
|
|
3
|
+
// helper (koffi, the relay's own lock check) under General.
|
|
4
|
+
//
|
|
5
|
+
// The relay installs these on demand from pins it ships (agent-sdk-store.mjs),
|
|
6
|
+
// so a row is mostly a window on the store: its state, the pinned and the
|
|
7
|
+
// installed version, the live npm log, the last error and the next automatic
|
|
8
|
+
// retry, and the one button that state calls for. Nothing here composes a
|
|
9
|
+
// command; the request carries an SDK id and the relay looks it up.
|
|
10
|
+
//
|
|
11
|
+
// Every visible state comes from the `agent_sdk_state` broadcast (the same
|
|
12
|
+
// payload as GET /api/agent-sdks/status), so an install survives closing the
|
|
13
|
+
// modal or being started by a first chat or another device. Rendering reads
|
|
14
|
+
// the latest payload and never assumes a local step sequence; the log is
|
|
15
|
+
// redrawn from the whole buffer whenever `logSeq` moves.
|
|
16
|
+
//
|
|
17
|
+
// The markup holds only empty mounts (`data-agent-sdk="<id>"`); this module
|
|
18
|
+
// draws the row into each. One SDK can have several mounts (Claude's), so the
|
|
19
|
+
// parts are found by `data-part`, not by id. The log is host process output:
|
|
20
|
+
// it goes in with textContent, and the little that goes through innerHTML is
|
|
21
|
+
// static and escaped with escHtml anyway.
|
|
22
|
+
|
|
23
|
+
import { escHtml } from './store.js';
|
|
24
|
+
import { cancelAgentSdkInstall, getAgentSdkStatus, startAgentSdkInstall } from './api-client.js';
|
|
25
|
+
import { updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
26
|
+
|
|
27
|
+
export const AGENT_SDK_IDS = Object.freeze(['claude', 'cursor', 'koffi']);
|
|
28
|
+
const STATES = Object.freeze(['missing', 'installing', 'ready', 'error', 'unsupported']);
|
|
29
|
+
const ACTIONS = Object.freeze(['install', 'retry', 'reinstall', 'cancel']);
|
|
30
|
+
const ACTION_LABELS = Object.freeze({ install: 'Install', retry: 'Retry', reinstall: 'Reinstall', cancel: 'Cancel' });
|
|
31
|
+
// Until the first payload names them.
|
|
32
|
+
const DEFAULT_LABELS = Object.freeze({ claude: 'Claude Agent SDK', cursor: 'Cursor SDK', koffi: 'Windows helper' });
|
|
33
|
+
const STATE_WORDS = Object.freeze({
|
|
34
|
+
missing: 'Not installed',
|
|
35
|
+
installing: 'Installing…',
|
|
36
|
+
ready: 'Installed',
|
|
37
|
+
error: 'Install failed',
|
|
38
|
+
unsupported: 'Not available on this platform',
|
|
39
|
+
});
|
|
40
|
+
const LOG_WORDS = Object.freeze({ installing: 'Running', ready: 'Finished', error: 'Failed' });
|
|
41
|
+
|
|
42
|
+
let sdkState = { root: null, host: null, sdks: {} };
|
|
43
|
+
let loadFailed = false;
|
|
44
|
+
// A request the relay refused before any broadcast, on the row that sent it;
|
|
45
|
+
// the SDK's next transition clears it.
|
|
46
|
+
let localErrors = {};
|
|
47
|
+
const requestsInFlight = new Set();
|
|
48
|
+
// One-shot per failed install: its log opens once, and a later redraw of the
|
|
49
|
+
// same payload does not fight the user closing it.
|
|
50
|
+
const expandedFailures = new Map();
|
|
51
|
+
let statusRefresh = null;
|
|
52
|
+
|
|
53
|
+
function normalizeId(raw) {
|
|
54
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
55
|
+
return AGENT_SDK_IDS.includes(value) ? value : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeState(raw) {
|
|
59
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
60
|
+
return STATES.includes(value) ? value : 'missing';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function labelOf(id, status) {
|
|
64
|
+
return String(status?.label || '').trim() || DEFAULT_LABELS[id] || id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The Windows helper only where it can exist; the SDK rows always. */
|
|
68
|
+
export function isAgentSdkRowShown(id, snapshot) {
|
|
69
|
+
if (normalizeId(id) !== 'koffi') return true;
|
|
70
|
+
const status = snapshot?.sdks?.koffi;
|
|
71
|
+
if (!status || typeof status !== 'object') return false;
|
|
72
|
+
if (status.support) return status.support !== 'no';
|
|
73
|
+
return snapshot?.host?.platform === 'win32';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** "Installed · pinned 0.3.1 · installed 0.3.1", with whose install it waits for. */
|
|
77
|
+
export function agentSdkStatusText(status) {
|
|
78
|
+
if (!status || typeof status !== 'object') return 'Checking…';
|
|
79
|
+
const state = normalizeState(status.state);
|
|
80
|
+
const heldBy = Number(status.heldBy) || 0;
|
|
81
|
+
const parts = [state === 'installing' && heldBy
|
|
82
|
+
? `Installing by another OAR process (pid ${heldBy})…`
|
|
83
|
+
: STATE_WORDS[state]];
|
|
84
|
+
const pinned = String(status.pinned || '').trim();
|
|
85
|
+
const installed = String(status.installed || '').trim();
|
|
86
|
+
if (pinned) parts.push(`pinned ${pinned}`);
|
|
87
|
+
if (installed) parts.push(`installed ${installed}`);
|
|
88
|
+
else if (state !== 'unsupported') parts.push('none installed');
|
|
89
|
+
return parts.join(' · ');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function agentSdkStatusState(status) {
|
|
93
|
+
if (!status || typeof status !== 'object') return 'pending';
|
|
94
|
+
const state = normalizeState(status.state);
|
|
95
|
+
if (state === 'ready') return 'active';
|
|
96
|
+
if (state === 'error' && status.kind !== 'cancelled') return 'error';
|
|
97
|
+
if (state === 'installing') return 'pending';
|
|
98
|
+
return 'unconfigured';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatRetryTime(iso) {
|
|
102
|
+
const at = new Date(iso);
|
|
103
|
+
if (Number.isNaN(at.getTime())) return '';
|
|
104
|
+
try {
|
|
105
|
+
return at.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
106
|
+
} catch {
|
|
107
|
+
return at.toISOString().slice(11, 16);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The line under the row: the error, the next automatic retry. */
|
|
112
|
+
export function agentSdkNoteText(status) {
|
|
113
|
+
if (!status || typeof status !== 'object') return '';
|
|
114
|
+
const state = normalizeState(status.state);
|
|
115
|
+
const parts = [];
|
|
116
|
+
// "Not available" is the whole story already in the status line.
|
|
117
|
+
const error = String(status.error || '').trim();
|
|
118
|
+
if (error && state === 'error') parts.push(error);
|
|
119
|
+
if (status.retryAt && state === 'error') {
|
|
120
|
+
const time = formatRetryTime(status.retryAt);
|
|
121
|
+
if (time) parts.push(`Next automatic retry at ${time}.`);
|
|
122
|
+
}
|
|
123
|
+
return parts.join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The buttons a row offers, in order. */
|
|
127
|
+
export function agentSdkActions(status, { reinstall = true } = {}) {
|
|
128
|
+
if (!status || typeof status !== 'object') return [];
|
|
129
|
+
switch (normalizeState(status.state)) {
|
|
130
|
+
case 'missing': return ['install'];
|
|
131
|
+
case 'installing': return ['cancel'];
|
|
132
|
+
case 'error': return ['retry'];
|
|
133
|
+
case 'ready': return reinstall ? ['reinstall'] : [];
|
|
134
|
+
default: return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function agentSdkLogSummary(status) {
|
|
139
|
+
const word = status?.kind === 'cancelled' ? 'Cancelled' : (LOG_WORDS[normalizeState(status?.state)] || 'Install');
|
|
140
|
+
return `${word} · install log`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function part(mount, name) {
|
|
144
|
+
return mount.querySelector(`[data-part="${name}"]`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function setHidden(element, hidden) {
|
|
148
|
+
if (element) element.hidden = !!hidden;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function mountSkeleton(mount, id) {
|
|
152
|
+
if (mount.dataset.agentSdkMounted === '1') return;
|
|
153
|
+
mount.dataset.agentSdkMounted = '1';
|
|
154
|
+
const buttons = ACTIONS.map((action) => (
|
|
155
|
+
`<button type="button" class="summary-btn" data-action="${escHtml(action)}" hidden>${escHtml(ACTION_LABELS[action])}</button>`
|
|
156
|
+
)).join('');
|
|
157
|
+
mount.innerHTML = `
|
|
158
|
+
<div class="settings-row" style="margin-bottom: 0;">
|
|
159
|
+
<span class="cli-install-label" data-part="label">${escHtml(DEFAULT_LABELS[id])}</span>
|
|
160
|
+
<div class="settings-help settings-provider-status" data-part="status" style="flex: 1 1 auto;">Checking…</div>
|
|
161
|
+
<div class="settings-inline-actions">${buttons}</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="settings-help settings-provider-status settings-gap-xs" data-part="note" hidden></div>
|
|
164
|
+
<details class="cli-install-log" data-part="log-details" hidden>
|
|
165
|
+
<summary data-part="log-summary">Install log</summary>
|
|
166
|
+
<pre data-part="log"></pre>
|
|
167
|
+
</details>
|
|
168
|
+
`;
|
|
169
|
+
for (const button of mount.querySelectorAll('button[data-action]')) {
|
|
170
|
+
button.addEventListener('click', () => { void runAgentSdkAction(id, button.dataset.action); });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderMount(mount, { expandFailure = false } = {}) {
|
|
175
|
+
const id = normalizeId(mount.dataset.agentSdk);
|
|
176
|
+
if (!id) return;
|
|
177
|
+
mountSkeleton(mount, id);
|
|
178
|
+
const status = sdkState.sdks?.[id] || null;
|
|
179
|
+
const shown = isAgentSdkRowShown(id, sdkState);
|
|
180
|
+
setHidden(mount, !shown);
|
|
181
|
+
if (!shown) return;
|
|
182
|
+
|
|
183
|
+
const label = part(mount, 'label');
|
|
184
|
+
if (label) label.textContent = labelOf(id, status);
|
|
185
|
+
|
|
186
|
+
const statusLine = part(mount, 'status');
|
|
187
|
+
if (statusLine) {
|
|
188
|
+
// The same words as the store's error, which the note shows already.
|
|
189
|
+
const localError = localErrors[id] && localErrors[id] !== String(status?.error || '').trim() ? localErrors[id] : '';
|
|
190
|
+
statusLine.textContent = localError
|
|
191
|
+
|| (!status && loadFailed ? 'Unable to load the SDK status.' : agentSdkStatusText(status));
|
|
192
|
+
statusLine.dataset.state = localError || (!status && loadFailed) ? 'error' : agentSdkStatusState(status);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The Windows helper has nothing to reinstall: its state and a Retry.
|
|
196
|
+
const offered = agentSdkActions(status, { reinstall: id !== 'koffi' });
|
|
197
|
+
const busy = requestsInFlight.has(id);
|
|
198
|
+
for (const button of mount.querySelectorAll('button[data-action]')) {
|
|
199
|
+
const action = button.dataset.action;
|
|
200
|
+
setHidden(button, !offered.includes(action));
|
|
201
|
+
// canInstall is false while spawns are off or the folder is unusable;
|
|
202
|
+
// Cancel needs nothing but the install it stops.
|
|
203
|
+
button.disabled = busy || (action !== 'cancel' && status?.canInstall === false);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const note = part(mount, 'note');
|
|
207
|
+
const noteText = agentSdkNoteText(status);
|
|
208
|
+
if (note) {
|
|
209
|
+
note.textContent = noteText;
|
|
210
|
+
note.dataset.state = noteText && status?.kind !== 'cancelled' ? 'error' : '';
|
|
211
|
+
}
|
|
212
|
+
setHidden(note, !noteText);
|
|
213
|
+
|
|
214
|
+
const details = part(mount, 'log-details');
|
|
215
|
+
const log = String(status?.log || '');
|
|
216
|
+
const showLog = !!status && (normalizeState(status.state) === 'installing' || log.length > 0);
|
|
217
|
+
setHidden(details, !showLog);
|
|
218
|
+
const summary = part(mount, 'log-summary');
|
|
219
|
+
if (summary && status) summary.textContent = agentSdkLogSummary(status);
|
|
220
|
+
const logBox = part(mount, 'log');
|
|
221
|
+
// Keyed on the install, not on logSeq alone: a new install starts with an
|
|
222
|
+
// empty buffer.
|
|
223
|
+
const logKey = `${status?.startedAt || ''}|${status?.logSeq || 0}`;
|
|
224
|
+
if (logBox && showLog && mount.dataset.logKey !== logKey) {
|
|
225
|
+
logBox.textContent = log;
|
|
226
|
+
logBox.scrollTop = logBox.scrollHeight;
|
|
227
|
+
mount.dataset.logKey = logKey;
|
|
228
|
+
}
|
|
229
|
+
if (details && showLog && expandFailure) details.open = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function renderAgentSdkRows() {
|
|
233
|
+
if (typeof document === 'undefined') return;
|
|
234
|
+
const expand = new Set();
|
|
235
|
+
for (const id of AGENT_SDK_IDS) {
|
|
236
|
+
const status = sdkState.sdks?.[id];
|
|
237
|
+
if (!status || normalizeState(status.state) !== 'error' || !status.log) continue;
|
|
238
|
+
const signature = `${status.startedAt || ''}|${status.finishedAt || ''}`;
|
|
239
|
+
if (expandedFailures.get(id) === signature) continue;
|
|
240
|
+
expandedFailures.set(id, signature);
|
|
241
|
+
if (status.kind !== 'cancelled') expand.add(id);
|
|
242
|
+
}
|
|
243
|
+
for (const mount of document.querySelectorAll('[data-agent-sdk]')) {
|
|
244
|
+
renderMount(mount, { expandFailure: expand.has(normalizeId(mount.dataset.agentSdk)) });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A broadcast can outrun the HTTP response that started it. Within one
|
|
249
|
+
// install (same startedAt) logSeq only grows, so an older snapshot never
|
|
250
|
+
// regresses the log or the state.
|
|
251
|
+
function isStaleStatus(current, incoming) {
|
|
252
|
+
if (!current || !incoming) return false;
|
|
253
|
+
if (current.startedAt && incoming.startedAt && incoming.startedAt < current.startedAt) return true;
|
|
254
|
+
if (!current.startedAt || current.startedAt !== incoming.startedAt) return false;
|
|
255
|
+
const currentSeq = Number(current.logSeq || 0);
|
|
256
|
+
const incomingSeq = Number(incoming.logSeq || 0);
|
|
257
|
+
if (incomingSeq < currentSeq) return true;
|
|
258
|
+
// Equal: a settled install is not reopened by the slower answer that
|
|
259
|
+
// snapshotted its start.
|
|
260
|
+
return incomingSeq === currentSeq
|
|
261
|
+
&& normalizeState(current.state) !== 'installing'
|
|
262
|
+
&& normalizeState(incoming.state) === 'installing';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Single entry point for every payload (GET, POST answers, the broadcast).
|
|
267
|
+
* It also feeds provider readiness, so a failed SDK shows up wherever that is
|
|
268
|
+
* asked.
|
|
269
|
+
*/
|
|
270
|
+
export function applyAgentSdkState(payload) {
|
|
271
|
+
if (!payload || typeof payload !== 'object' || !payload.sdks || typeof payload.sdks !== 'object') {
|
|
272
|
+
renderAgentSdkRows();
|
|
273
|
+
return sdkState;
|
|
274
|
+
}
|
|
275
|
+
const sdks = { ...sdkState.sdks };
|
|
276
|
+
for (const id of AGENT_SDK_IDS) {
|
|
277
|
+
const incoming = payload.sdks[id];
|
|
278
|
+
if (!incoming || typeof incoming !== 'object' || isStaleStatus(sdks[id], incoming)) continue;
|
|
279
|
+
const before = sdks[id];
|
|
280
|
+
if (!before || before.state !== incoming.state || before.startedAt !== incoming.startedAt) delete localErrors[id];
|
|
281
|
+
sdks[id] = incoming;
|
|
282
|
+
}
|
|
283
|
+
sdkState = {
|
|
284
|
+
root: typeof payload.root === 'string' ? payload.root : sdkState.root,
|
|
285
|
+
host: payload.host && typeof payload.host === 'object' ? payload.host : sdkState.host,
|
|
286
|
+
sdks,
|
|
287
|
+
};
|
|
288
|
+
loadFailed = false;
|
|
289
|
+
updateProviderReadinessInputs({ agentSdks: sdkState });
|
|
290
|
+
renderAgentSdkRows();
|
|
291
|
+
return sdkState;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getAgentSdkState() {
|
|
295
|
+
return sdkState;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Reads the rows; a second caller joins the read in flight. */
|
|
299
|
+
export function refreshAgentSdkRows() {
|
|
300
|
+
if (statusRefresh) return statusRefresh;
|
|
301
|
+
const promise = (async () => {
|
|
302
|
+
try {
|
|
303
|
+
const payload = await getAgentSdkStatus();
|
|
304
|
+
if (!payload) {
|
|
305
|
+
loadFailed = true;
|
|
306
|
+
renderAgentSdkRows();
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return applyAgentSdkState(payload);
|
|
310
|
+
} finally {
|
|
311
|
+
if (statusRefresh === promise) statusRefresh = null;
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
statusRefresh = promise;
|
|
315
|
+
return promise;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Install, Retry, Reinstall or Cancel, from a row's button. */
|
|
319
|
+
export async function runAgentSdkAction(sdk, action) {
|
|
320
|
+
const id = normalizeId(sdk);
|
|
321
|
+
const wanted = ACTIONS.includes(action) ? action : '';
|
|
322
|
+
if (!id || !wanted || requestsInFlight.has(id)) return;
|
|
323
|
+
requestsInFlight.add(id);
|
|
324
|
+
delete localErrors[id];
|
|
325
|
+
renderAgentSdkRows();
|
|
326
|
+
try {
|
|
327
|
+
const payload = wanted === 'cancel'
|
|
328
|
+
? await cancelAgentSdkInstall(id)
|
|
329
|
+
: await startAgentSdkInstall(id, { reinstall: wanted === 'reinstall' });
|
|
330
|
+
applyAgentSdkState(payload);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
localErrors = { ...localErrors, [id]: String(error?.message || 'The SDK request failed.') };
|
|
333
|
+
// The refusal carried the rows, but as a throw: read them again.
|
|
334
|
+
await refreshAgentSdkRows().catch(() => {});
|
|
335
|
+
} finally {
|
|
336
|
+
requestsInFlight.delete(id);
|
|
337
|
+
renderAgentSdkRows();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Tests only. */
|
|
342
|
+
export function resetAgentSdkUiForTests() {
|
|
343
|
+
sdkState = { root: null, host: null, sdks: {} };
|
|
344
|
+
loadFailed = false;
|
|
345
|
+
localErrors = {};
|
|
346
|
+
requestsInFlight.clear();
|
|
347
|
+
expandedFailures.clear();
|
|
348
|
+
statusRefresh = null;
|
|
349
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BASE, TOKEN, CLIENT_ID, IS_SHARE_PREVIEW, SHARE_PREVIEW_CONVERSATION_ID, SHARED_CONVERSATION_TOKEN, authHeaders, updateWorkspaceRootHints, applyContextUsageBar, readContextUsageRatio, currentConvId, conversations, setCliOnline, setCloudflaredTunnelState, setActiveRuntimeSessionCount, setRuntimeSessionBindingCount, setContextIndicatorMode, setServerPlatform, setUpdateState } from './store.js';
|
|
2
2
|
import { liveActivityCap } from './live-activity-cap.mjs';
|
|
3
|
+
import { OPENAI_IMAGE_PRESETS_PATH, OPENAI_IMAGE_SETTINGS_FIELDS, OPENAI_IMAGE_USAGE_PATH } from './openai-image-spend.mjs';
|
|
3
4
|
|
|
4
5
|
// fetch() has no default timeout. Without one, a request issued while the app is
|
|
5
6
|
// being backgrounded can hang indefinitely on a half-open mobile connection and
|
|
@@ -331,6 +332,8 @@ export async function updateClaudeSettings({
|
|
|
331
332
|
enabled = undefined,
|
|
332
333
|
model = undefined,
|
|
333
334
|
enabledModels = undefined,
|
|
335
|
+
shownModels = undefined,
|
|
336
|
+
autoEnableNew = undefined,
|
|
334
337
|
attributionMode = undefined,
|
|
335
338
|
backgroundCommandLimitMinutes = undefined,
|
|
336
339
|
} = {}) {
|
|
@@ -338,6 +341,8 @@ export async function updateClaudeSettings({
|
|
|
338
341
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
339
342
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
340
343
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
344
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
345
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
341
346
|
if (typeof attributionMode === 'string' && attributionMode.trim()) payload.attributionMode = attributionMode.trim();
|
|
342
347
|
if (Number.isFinite(backgroundCommandLimitMinutes)) payload.backgroundCommandLimitMinutes = backgroundCommandLimitMinutes;
|
|
343
348
|
return settingsRequest('/api/settings/claude', payload, 'Failed to update Claude settings');
|
|
@@ -393,6 +398,25 @@ export async function cancelCliInstall() {
|
|
|
393
398
|
return settingsRequest('/api/cli/install/cancel', null, 'Failed to cancel the CLI install');
|
|
394
399
|
}
|
|
395
400
|
|
|
401
|
+
// Agent SDKs the relay installs on demand. As for the CLIs, the body carries
|
|
402
|
+
// only an SDK id, which the relay looks up in its own table.
|
|
403
|
+
export async function getAgentSdkStatus() {
|
|
404
|
+
return apiFetch('/api/agent-sdks/status');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function startAgentSdkInstall(sdk, { reinstall = false } = {}) {
|
|
408
|
+
return settingsRequest(
|
|
409
|
+
'/api/agent-sdks/install',
|
|
410
|
+
{ sdk: String(sdk || ''), reinstall: reinstall === true },
|
|
411
|
+
'Failed to start the SDK install',
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Stops a running install, and dismisses a failure otherwise.
|
|
416
|
+
export async function cancelAgentSdkInstall(sdk) {
|
|
417
|
+
return settingsRequest('/api/agent-sdks/install/cancel', { sdk: String(sdk || '') }, 'Failed to cancel the SDK install');
|
|
418
|
+
}
|
|
419
|
+
|
|
396
420
|
// Grok account auth (Sign in / Sign out). One route shorter than Claude's: the
|
|
397
421
|
// device code rides in the authorize URL and the CLI polls x.ai itself, so
|
|
398
422
|
// nothing is ever pasted back through the relay.
|
|
@@ -424,11 +448,15 @@ export async function updateGrokSettings({
|
|
|
424
448
|
enabled = undefined,
|
|
425
449
|
model = undefined,
|
|
426
450
|
enabledModels = undefined,
|
|
451
|
+
shownModels = undefined,
|
|
452
|
+
autoEnableNew = undefined,
|
|
427
453
|
} = {}) {
|
|
428
454
|
const payload = {};
|
|
429
455
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
430
456
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
431
457
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
458
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
459
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
432
460
|
return settingsRequest('/api/settings/grok', payload, 'Failed to update Grok settings');
|
|
433
461
|
}
|
|
434
462
|
|
|
@@ -493,21 +521,86 @@ export async function loadClaudeCloudBranches(repo) {
|
|
|
493
521
|
|
|
494
522
|
export async function updateOpenAISettings({
|
|
495
523
|
apiKey = '',
|
|
496
|
-
model =
|
|
524
|
+
model = undefined,
|
|
497
525
|
baseUrl = undefined,
|
|
498
526
|
enabled = undefined,
|
|
527
|
+
enabledModels = undefined,
|
|
528
|
+
shownModels = undefined,
|
|
529
|
+
autoEnableNew = undefined,
|
|
499
530
|
remove = false,
|
|
500
531
|
} = {}) {
|
|
501
532
|
const payload = {
|
|
502
533
|
apiKey: String(apiKey || '').trim(),
|
|
503
|
-
model: String(model || '').trim() || 'gpt-4o',
|
|
504
534
|
remove: remove === true,
|
|
505
535
|
};
|
|
536
|
+
// Every Settings call names the model; the Select Models save does not, and
|
|
537
|
+
// must not reset the default model to gpt-4o.
|
|
538
|
+
if (model !== undefined) payload.model = String(model || '').trim() || 'gpt-4o';
|
|
506
539
|
if (typeof baseUrl === 'string') payload.baseUrl = String(baseUrl).trim();
|
|
507
540
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
541
|
+
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
542
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
543
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
508
544
|
return settingsRequest('/api/settings/openai', payload, 'Failed to update OpenAI settings');
|
|
509
545
|
}
|
|
510
546
|
|
|
547
|
+
/**
|
|
548
|
+
* The daily image cap and today's override (POST /api/settings/openai with
|
|
549
|
+
* only these fields). Throws with the relay's words: a paired relay's
|
|
550
|
+
* request is refused (403).
|
|
551
|
+
*/
|
|
552
|
+
export async function updateOpenAIImageSpendSettings({ imageDailyCapUsd = undefined, allowImagesToday = undefined } = {}) {
|
|
553
|
+
const payload = {};
|
|
554
|
+
if (imageDailyCapUsd !== undefined) payload[OPENAI_IMAGE_SETTINGS_FIELDS.dailyCapUsd] = Number(imageDailyCapUsd);
|
|
555
|
+
if (typeof allowImagesToday === 'boolean') payload[OPENAI_IMAGE_SETTINGS_FIELDS.allowToday] = allowImagesToday;
|
|
556
|
+
return settingsRequest('/api/settings/openai', payload, 'Failed to update the image cap');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Today's image spend; with a conversation, that chat's cost too. Null on any failure. */
|
|
560
|
+
export async function loadOpenAIImageUsage({ conversationId = '' } = {}) {
|
|
561
|
+
const id = String(conversationId || '').trim();
|
|
562
|
+
return quietGet(id ? `${OPENAI_IMAGE_USAGE_PATH}?conversationId=${encodeURIComponent(id)}` : OPENAI_IMAGE_USAGE_PATH);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// The presets routes: the relay's `error` and `code` ride on the thrown Error.
|
|
566
|
+
async function imagePresetsRequest(method, path, body = null) {
|
|
567
|
+
if (!networkRequestsEnabled) return null;
|
|
568
|
+
let response;
|
|
569
|
+
try {
|
|
570
|
+
response = await fetch(`${BASE}${path}`, {
|
|
571
|
+
signal: requestTimeoutSignal(),
|
|
572
|
+
method,
|
|
573
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
574
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
575
|
+
});
|
|
576
|
+
} catch (error) {
|
|
577
|
+
noteFetchFailure(path, error);
|
|
578
|
+
throw Object.assign(new Error(`The relay could not be reached (${readableServerError(toErrorMessage(error))}).`), { status: 0, code: null });
|
|
579
|
+
}
|
|
580
|
+
noteFetchSuccess();
|
|
581
|
+
const result = await response.json().catch(() => null);
|
|
582
|
+
if (!response.ok) {
|
|
583
|
+
throw Object.assign(
|
|
584
|
+
new Error(readableServerError(result?.error || '') || `The relay answered HTTP ${response.status}.`),
|
|
585
|
+
{ status: response.status, code: result?.code || null },
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** `{ presets }` (built-ins and own), or null when the relay has none to give. */
|
|
592
|
+
export async function loadOpenAIImagePresets() {
|
|
593
|
+
return quietGet(OPENAI_IMAGE_PRESETS_PATH);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function saveOpenAIImagePreset(preset, { replace = false } = {}) {
|
|
597
|
+
return imagePresetsRequest('PUT', OPENAI_IMAGE_PRESETS_PATH, { preset, ...(replace ? { replace: true } : {}) });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export async function deleteOpenAIImagePreset(name) {
|
|
601
|
+
return imagePresetsRequest('DELETE', `${OPENAI_IMAGE_PRESETS_PATH}/${encodeURIComponent(String(name || ''))}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
511
604
|
export async function loadCursorSettings() {
|
|
512
605
|
return apiFetch('/api/settings/cursor');
|
|
513
606
|
}
|
|
@@ -517,6 +610,8 @@ export async function updateCursorSettings({
|
|
|
517
610
|
model = undefined,
|
|
518
611
|
enabled = undefined,
|
|
519
612
|
enabledModels = undefined,
|
|
613
|
+
shownModels = undefined,
|
|
614
|
+
autoEnableNew = undefined,
|
|
520
615
|
remove = false,
|
|
521
616
|
} = {}) {
|
|
522
617
|
const payload = {
|
|
@@ -528,6 +623,8 @@ export async function updateCursorSettings({
|
|
|
528
623
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
529
624
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
530
625
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
626
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
627
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
531
628
|
return settingsRequest('/api/settings/cursor', payload, 'Failed to update Cursor settings');
|
|
532
629
|
}
|
|
533
630
|
|
|
@@ -546,6 +643,8 @@ export async function updateDeepSeekSettings({
|
|
|
546
643
|
model = undefined,
|
|
547
644
|
enabled = undefined,
|
|
548
645
|
enabledModels = undefined,
|
|
646
|
+
shownModels = undefined,
|
|
647
|
+
autoEnableNew = undefined,
|
|
549
648
|
baseUrl = undefined,
|
|
550
649
|
attribution = undefined,
|
|
551
650
|
monthlyBudget = undefined,
|
|
@@ -563,6 +662,8 @@ export async function updateDeepSeekSettings({
|
|
|
563
662
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
564
663
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
565
664
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
665
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
666
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
566
667
|
if (baseUrl === null || typeof baseUrl === 'string') payload.baseUrl = baseUrl === null ? null : String(baseUrl).trim();
|
|
567
668
|
if (typeof attribution === 'string' && attribution.trim()) payload.attribution = attribution.trim();
|
|
568
669
|
if (monthlyBudget === null || (typeof monthlyBudget === 'number' && Number.isFinite(monthlyBudget))) payload.monthlyBudget = monthlyBudget;
|
|
@@ -590,6 +691,8 @@ export async function updateClaudeApiSettings({
|
|
|
590
691
|
model = undefined,
|
|
591
692
|
enabled = undefined,
|
|
592
693
|
enabledModels = undefined,
|
|
694
|
+
shownModels = undefined,
|
|
695
|
+
autoEnableNew = undefined,
|
|
593
696
|
monthlyCapUsd = undefined,
|
|
594
697
|
remove = false,
|
|
595
698
|
} = {}) {
|
|
@@ -602,6 +705,8 @@ export async function updateClaudeApiSettings({
|
|
|
602
705
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
603
706
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
604
707
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
708
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
709
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
605
710
|
if (monthlyCapUsd === null || (typeof monthlyCapUsd === 'number' && Number.isFinite(monthlyCapUsd))) payload.monthlyCapUsd = monthlyCapUsd;
|
|
606
711
|
}
|
|
607
712
|
return settingsRequest('/api/settings/claude-api', payload, 'Failed to update Claude API settings');
|
|
@@ -940,14 +1045,50 @@ export async function refreshModelVariantCatalog() {
|
|
|
940
1045
|
return apiFetch('/api/model-variants/refresh', { method: 'POST' });
|
|
941
1046
|
}
|
|
942
1047
|
|
|
943
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* Copilot's selection and its "Turn new models on automatically". A null
|
|
1050
|
+
* list leaves the selection alone (only the switch changes): an empty one is
|
|
1051
|
+
* refused, at least one variant stays on. `shownVariantIds` names the rows
|
|
1052
|
+
* the save could see: the relay turns only those on or off.
|
|
1053
|
+
*/
|
|
1054
|
+
export async function saveEnabledModelVariants(enabledVariantIds = [], { autoEnableNew = undefined, shownVariantIds = undefined } = {}) {
|
|
1055
|
+
const payload = {};
|
|
1056
|
+
if (enabledVariantIds !== null) {
|
|
1057
|
+
payload.enabledVariantIds = Array.isArray(enabledVariantIds)
|
|
1058
|
+
? enabledVariantIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
1059
|
+
: [];
|
|
1060
|
+
}
|
|
1061
|
+
if (Array.isArray(shownVariantIds)) {
|
|
1062
|
+
payload.shownVariantIds = shownVariantIds.map((value) => String(value || '').trim()).filter(Boolean);
|
|
1063
|
+
}
|
|
1064
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
944
1065
|
return apiFetch('/api/model-variants', {
|
|
945
1066
|
method: 'PATCH',
|
|
946
|
-
body: JSON.stringify(
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1067
|
+
body: JSON.stringify(payload),
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Opening Select Models clears every provider's "New" tags and the menu dot;
|
|
1072
|
+
// answers `{ ok, unseenNewModelCount }`.
|
|
1073
|
+
export async function markModelsSeen() {
|
|
1074
|
+
return apiFetch('/api/model-variants/seen', { method: 'POST' });
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Settings › General › Models: the background check for new models. A failed
|
|
1078
|
+
// read throws with the relay's reason, which the section shows.
|
|
1079
|
+
export async function loadModelRefreshSettings() {
|
|
1080
|
+
return apiFetchOrThrow('/api/model-refresh-settings');
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** Any of `{ auto, intervalHours, excluded }`; a refused value throws with the relay's reason. */
|
|
1084
|
+
export async function updateModelRefreshSettings(patch = {}) {
|
|
1085
|
+
const payload = {};
|
|
1086
|
+
if (typeof patch.auto === 'boolean') payload.auto = patch.auto;
|
|
1087
|
+
if (patch.intervalHours !== undefined) payload.intervalHours = Number(patch.intervalHours);
|
|
1088
|
+
if (Array.isArray(patch.excluded)) payload.excluded = patch.excluded.map((value) => String(value || '').trim()).filter(Boolean);
|
|
1089
|
+
return apiFetchOrThrow('/api/model-refresh-settings', {
|
|
1090
|
+
method: 'POST',
|
|
1091
|
+
body: JSON.stringify(payload),
|
|
951
1092
|
});
|
|
952
1093
|
}
|
|
953
1094
|
|
|
@@ -131,7 +131,8 @@ export async function loadRelayQuestions(conversationId) {
|
|
|
131
131
|
// after a link the relay did not know: its notice keeps the screen empty
|
|
132
132
|
// until the reader opens something.
|
|
133
133
|
const readingAView = document.body?.classList?.contains('inbox-view-active')
|
|
134
|
-
|| document.body?.classList?.contains('status-view-active')
|
|
134
|
+
|| document.body?.classList?.contains('status-view-active')
|
|
135
|
+
|| document.body?.classList?.contains('media-view-active');
|
|
135
136
|
if (!conversationId && pending.length && !readingAView && !isConversationAutoOpenHeld()) {
|
|
136
137
|
const latest = pending
|
|
137
138
|
.slice()
|