@oar-sh/oar 0.9.16 → 0.9.18
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/polling/polling-loop.mjs +14 -87
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +412 -0
- package/README.md +15 -13
- package/package.json +2 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
- package/server/claude-worker/claude-session-process.mjs +276 -33
- package/server/claude-worker/claude-session-worker-main.mjs +269 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +394 -13
- package/server/claude-worker/claude-usage-totals.mjs +161 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +96 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +105 -0
- package/server/public/app/bootstrap.js +243 -29
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +10 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +18 -0
- package/server/public/app/conversation-view.js +31 -3
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +156 -11
- package/server/public/app/model-selector-options.mjs +28 -1
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +8 -2
- package/server/public/app/relay-error-ctas.mjs +58 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +22 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +28 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +596 -35
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +14 -7
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +3 -2
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +831 -36
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +670 -63
- package/server/server-runtime.mjs +476 -26
- package/server/services/about-service.mjs +2 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +436 -0
- package/server/services/provider-model-selection.mjs +4 -2
- package/server/services/provider-reasoning-effort.mjs +24 -2
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +84 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +294 -84
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +320 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +2 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -35,6 +35,7 @@ const TAB_LABELS = Object.freeze(Object.assign(Object.create(null), {
|
|
|
35
35
|
'claude-cloud': 'Claude Cloud',
|
|
36
36
|
cursor: 'Cursor',
|
|
37
37
|
grok: 'Grok',
|
|
38
|
+
deepseek: 'DeepSeek',
|
|
38
39
|
}));
|
|
39
40
|
|
|
40
41
|
// The composer speaks 'github'/'openai'/… while the usage report keys Copilot as
|
|
@@ -86,9 +87,9 @@ export function utilizationColor(utilization) {
|
|
|
86
87
|
export function formatAmount(value, unit) {
|
|
87
88
|
const n = toNullableNumber(value);
|
|
88
89
|
if (n === null) return null;
|
|
89
|
-
if (unit === 'usd') {
|
|
90
|
+
if (unit === 'usd' || unit === 'cny') {
|
|
90
91
|
const rounded = Math.round(n * 100) / 100;
|
|
91
|
-
return `${rounded < 0 ? '-' : ''}
|
|
92
|
+
return `${rounded < 0 ? '-' : ''}${unit === 'cny' ? '¥' : '$'}${Math.abs(rounded).toFixed(2)}`;
|
|
92
93
|
}
|
|
93
94
|
if (unit === 'percent') return `${Math.round(n * 10) / 10}%`;
|
|
94
95
|
const abs = Math.abs(n);
|
|
@@ -189,15 +190,103 @@ function renderMeter(meter, now) {
|
|
|
189
190
|
`;
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
/**
|
|
194
|
+
* A chart from `buildChart` data: stacked `columns` (one per item, e.g. a
|
|
195
|
+
* day) or horizontal stacked `bars` (one per item, e.g. a model), drawn with
|
|
196
|
+
* plain HTML and CSS. Colours come from the theme's series variables (fixed
|
|
197
|
+
* order, so a series keeps its colour between reads); every item has a text
|
|
198
|
+
* equivalent as its label, a tooltip on hover, focus or tap, and the chart
|
|
199
|
+
* has a legend for more than one series and a table view of the same data.
|
|
200
|
+
*/
|
|
201
|
+
function renderChart(chart) {
|
|
202
|
+
const series = Array.isArray(chart?.series) ? chart.series.filter((entry) => entry && entry.key) : [];
|
|
203
|
+
const items = Array.isArray(chart?.items) ? chart.items.filter((entry) => entry && entry.key) : [];
|
|
204
|
+
if (!series.length || !items.length) return '';
|
|
205
|
+
const unit = chart.unit || 'credits';
|
|
206
|
+
// Money under a cent keeps four decimals, as the card's estimates do.
|
|
207
|
+
const amount = (value) => {
|
|
208
|
+
const n = toNullableNumber(value) ?? 0;
|
|
209
|
+
if ((unit === 'usd' || unit === 'cny') && n !== 0 && Math.abs(n) < 0.01) {
|
|
210
|
+
return `${n < 0 ? '-' : ''}${unit === 'cny' ? '¥' : '$'}${Math.abs(n).toFixed(4)}`;
|
|
211
|
+
}
|
|
212
|
+
return formatAmount(n, unit) ?? '0';
|
|
213
|
+
};
|
|
214
|
+
const valueOf = (item, key) => Math.max(0, toNullableNumber(item.values?.[key]) ?? 0);
|
|
215
|
+
const totalOf = (item) => series.reduce((sum, entry) => sum + valueOf(item, entry.key), 0);
|
|
216
|
+
const seriesColor = (index) => `var(--plan-usage-series-${Math.min(index + 1, 5)})`;
|
|
217
|
+
const partsText = (item) => {
|
|
218
|
+
const total = totalOf(item);
|
|
219
|
+
return series
|
|
220
|
+
.map((entry) => ({ entry, value: valueOf(item, entry.key) }))
|
|
221
|
+
.filter(({ value }) => value > 0)
|
|
222
|
+
.map(({ entry, value }) => `${entry.label} ${amount(value)}${series.length > 1 && total > 0 ? ` (${Math.round((value / total) * 100)}%)` : ''}`)
|
|
223
|
+
.join(', ');
|
|
224
|
+
};
|
|
225
|
+
const itemText = (item) => {
|
|
226
|
+
const total = totalOf(item);
|
|
227
|
+
if (total <= 0) return `${item.label}: nothing`;
|
|
228
|
+
const parts = series.length > 1 ? partsText(item) : '';
|
|
229
|
+
return `${item.label}: ${amount(total)}${parts ? ` (${parts})` : ''}`;
|
|
230
|
+
};
|
|
231
|
+
const max = Math.max(0, ...items.map(totalOf));
|
|
232
|
+
const segments = (item, dimension) => series.map((entry, index) => {
|
|
233
|
+
const value = valueOf(item, entry.key);
|
|
234
|
+
if (value <= 0) return '';
|
|
235
|
+
const base = dimension === 'height' ? max : totalOf(item);
|
|
236
|
+
const share = base > 0 ? (value / base) * 100 : 0;
|
|
237
|
+
return `<span class="plan-usage-chart-seg" data-series="${escapeHtml(entry.key)}" style="${dimension}:${share.toFixed(2)}%;background:${seriesColor(index)}" title="${escapeHtml(`${entry.label} ${amount(value)}`)}"></span>`;
|
|
238
|
+
}).join('');
|
|
239
|
+
|
|
240
|
+
const plot = chart.kind === 'bars'
|
|
241
|
+
? `<div class="plan-usage-chart-bars">${items.map((item) => `
|
|
242
|
+
<div class="plan-usage-chart-row" tabindex="0" aria-label="${escapeHtml(itemText(item))}">
|
|
243
|
+
<span class="plan-usage-chart-row-label">${escapeHtml(item.label)}</span>
|
|
244
|
+
<span class="plan-usage-chart-bar" aria-hidden="true">${segments(item, 'width')}</span>
|
|
245
|
+
<span class="plan-usage-chart-row-value">${escapeHtml(amount(totalOf(item)))}</span>
|
|
246
|
+
<span class="plan-usage-chart-tip" role="tooltip">${escapeHtml(itemText(item))}</span>
|
|
247
|
+
</div>`).join('')}</div>`
|
|
248
|
+
: `<div class="plan-usage-chart-columns" style="--plan-usage-columns:${items.length}">${items.map((item) => `
|
|
249
|
+
<div class="plan-usage-chart-col${totalOf(item) > 0 ? '' : ' plan-usage-chart-col-empty'}" tabindex="0" aria-label="${escapeHtml(itemText(item))}" data-item="${escapeHtml(item.key)}">
|
|
250
|
+
<span class="plan-usage-chart-stack" aria-hidden="true">${segments(item, 'height')}</span>
|
|
251
|
+
<span class="plan-usage-chart-tip" role="tooltip">${escapeHtml(itemText(item))}</span>
|
|
252
|
+
</div>`).join('')}
|
|
253
|
+
<span class="plan-usage-chart-axis" aria-hidden="true"><span>${escapeHtml(items[0].label)}</span><span>${escapeHtml(items[items.length - 1].label)}</span></span>
|
|
254
|
+
</div>`;
|
|
255
|
+
|
|
256
|
+
const legend = series.length > 1
|
|
257
|
+
? `<ul class="plan-usage-chart-legend">${series.map((entry, index) => `<li><span class="plan-usage-chart-swatch" style="background:${seriesColor(index)}" aria-hidden="true"></span>${escapeHtml(entry.label)}</li>`).join('')}</ul>`
|
|
258
|
+
: '';
|
|
259
|
+
const table = `
|
|
260
|
+
<details class="plan-usage-chart-table">
|
|
261
|
+
<summary>As a table</summary>
|
|
262
|
+
<table class="plan-usage-detail-table">
|
|
263
|
+
<thead><tr><th scope="col"></th>${series.map((entry) => `<th scope="col">${escapeHtml(entry.label)}</th>`).join('')}${series.length > 1 ? '<th scope="col">Total</th>' : ''}</tr></thead>
|
|
264
|
+
<tbody>${items.filter((item) => totalOf(item) > 0).map((item) => `<tr><th scope="row">${escapeHtml(item.label)}</th>${series.map((entry) => `<td class="plan-usage-detail-value">${escapeHtml(amount(valueOf(item, entry.key)))}</td>`).join('')}${series.length > 1 ? `<td class="plan-usage-detail-value">${escapeHtml(amount(totalOf(item)))}</td>` : ''}</tr>`).join('')}</tbody>
|
|
265
|
+
</table>
|
|
266
|
+
</details>`;
|
|
267
|
+
return `
|
|
268
|
+
<figure class="plan-usage-chart plan-usage-chart-kind-${chart.kind === 'bars' ? 'bars' : 'columns'}" data-chart-id="${escapeHtml(chart.id)}" role="group" aria-label="${escapeHtml(chart.label)}">
|
|
269
|
+
<figcaption class="plan-usage-chart-head">
|
|
270
|
+
<span class="plan-usage-chart-label">${escapeHtml(chart.label)}</span>
|
|
271
|
+
${chart.summary ? `<span class="plan-usage-chart-summary">${escapeHtml(chart.summary)}</span>` : ''}
|
|
272
|
+
</figcaption>
|
|
273
|
+
${plot}
|
|
274
|
+
${legend}
|
|
275
|
+
${chart.note ? `<div class="plan-usage-meter-note">${escapeHtml(chart.note)}</div>` : ''}
|
|
276
|
+
${table}
|
|
277
|
+
</figure>
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
|
|
192
281
|
function renderDetailSection(section) {
|
|
193
282
|
const rows = section.rows.map((row) => `
|
|
194
283
|
<tr>
|
|
195
284
|
<td class="plan-usage-detail-label">${escapeHtml(row.label)}${row.hint ? `<span class="plan-usage-detail-hint">${escapeHtml(row.hint)}</span>` : ''}</td>
|
|
196
|
-
<td class="plan-usage-detail-value">${
|
|
285
|
+
<td class="plan-usage-detail-value">${String(row.value).split(' · ').map((part) => `<span class="plan-usage-detail-part">${escapeHtml(part)}</span>`).join(' · ')}</td>
|
|
197
286
|
</tr>
|
|
198
287
|
`).join('');
|
|
199
288
|
return `
|
|
200
|
-
<details class="plan-usage-details" data-section-id="${escapeHtml(section.id)}">
|
|
289
|
+
<details class="plan-usage-details" data-section-id="${escapeHtml(section.id)}"${section.open ? ' open' : ''}>
|
|
201
290
|
<summary>${escapeHtml(section.label)}</summary>
|
|
202
291
|
${section.note ? `<div class="plan-usage-detail-note">${escapeHtml(section.note)}</div>` : ''}
|
|
203
292
|
<table class="plan-usage-detail-table"><tbody>${rows}</tbody></table>
|
|
@@ -205,9 +294,59 @@ function renderDetailSection(section) {
|
|
|
205
294
|
`;
|
|
206
295
|
}
|
|
207
296
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Hours of the day the card note names, shown in the reader's own time zone.
|
|
299
|
+
* `utcRanges` are `[start, end)` whole hours in UTC; the day is `now`'s, so
|
|
300
|
+
* daylight-saving time is the reader's current one. "03:00–06:00 and
|
|
301
|
+
* 08:00–12:00 in your time zone" for `[[1, 4], [6, 10]]` read in Berlin in
|
|
302
|
+
* summer. `timeZone` is for tests; the browser leaves it to Intl.
|
|
303
|
+
*/
|
|
304
|
+
export function formatLocalHourRanges(utcRanges, { now = new Date(), timeZone = undefined } = {}) {
|
|
305
|
+
if (!Array.isArray(utcRanges) || !utcRanges.length) return null;
|
|
306
|
+
let formatter;
|
|
307
|
+
try {
|
|
308
|
+
formatter = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZone });
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const day = now instanceof Date && !Number.isNaN(now.getTime()) ? now : new Date();
|
|
313
|
+
const clock = (hour) => formatter.format(new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), hour, 0, 0)));
|
|
314
|
+
const parts = [];
|
|
315
|
+
for (const range of utcRanges) {
|
|
316
|
+
if (!Array.isArray(range) || range.length !== 2) return null;
|
|
317
|
+
const start = Number(range[0]);
|
|
318
|
+
const end = Number(range[1]);
|
|
319
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > 24 || start >= end) return null;
|
|
320
|
+
parts.push(`${clock(start)}–${clock(end)}`);
|
|
321
|
+
}
|
|
322
|
+
const joined = parts.length > 1 ? `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}` : parts[0];
|
|
323
|
+
return `${joined} in your time zone`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The note's text with its UTC hours replaced by the reader's local ones, when it names any. */
|
|
327
|
+
function noteTextForReader(note, { now, timeZone } = {}) {
|
|
210
328
|
const text = String(note?.text ?? '').trim();
|
|
329
|
+
const utcText = String(note?.hours?.utcText ?? '').trim();
|
|
330
|
+
if (!text || !utcText || !text.includes(utcText)) return text;
|
|
331
|
+
const local = formatLocalHourRanges(note.hours.utcRanges, { now, timeZone });
|
|
332
|
+
return local ? text.split(utcText).join(local) : text;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A note that is a figure (buildCardNote's label and value): one row of the
|
|
337
|
+
* card's figures, the hint under the label, the whole sentence as its title.
|
|
338
|
+
*/
|
|
339
|
+
function renderFact(note) {
|
|
340
|
+
const hint = String(note?.hint ?? '').trim();
|
|
341
|
+
return `<div class="plan-usage-fact" data-note-id="${escapeHtml(note?.id ?? '')}" title="${escapeHtml(String(note?.text ?? '').trim())}">`
|
|
342
|
+
+ `<dt>${escapeHtml(note.label)}${hint ? `<span class="plan-usage-fact-hint">${escapeHtml(hint)}</span>` : ''}</dt>`
|
|
343
|
+
+ `<dd>${escapeHtml(note.value)}</dd></div>`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const isFactNote = (note) => Boolean(String(note?.label ?? '').trim() && String(note?.value ?? '').trim());
|
|
347
|
+
|
|
348
|
+
function renderNote(note, { now, timeZone } = {}) {
|
|
349
|
+
const text = noteTextForReader(note, { now, timeZone });
|
|
211
350
|
if (!text) return '';
|
|
212
351
|
const url = String(note?.link?.url ?? '').trim();
|
|
213
352
|
const label = String(note?.link?.label ?? '').trim();
|
|
@@ -215,7 +354,11 @@ function renderNote(note) {
|
|
|
215
354
|
const link = label && /^https:\/\//i.test(url)
|
|
216
355
|
? ` <a class="plan-usage-link" href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`
|
|
217
356
|
: '';
|
|
218
|
-
|
|
357
|
+
// The UTC wording stays on the element for a reader who wants it.
|
|
358
|
+
const utcTitle = note?.hours?.utcText && text !== String(note.text ?? '').trim()
|
|
359
|
+
? ` title="${escapeHtml(String(note.text).trim())}"`
|
|
360
|
+
: '';
|
|
361
|
+
return `<div class="plan-usage-detail-note plan-usage-note" data-note-id="${escapeHtml(note?.id ?? '')}"${utcTitle}>${escapeHtml(text)}${link}</div>`;
|
|
219
362
|
}
|
|
220
363
|
|
|
221
364
|
/**
|
|
@@ -240,10 +383,18 @@ function renderAccountLine(account) {
|
|
|
240
383
|
return `<span class="plan-usage-account">${parts.map(escapeHtml).join(' · ')}</span>`;
|
|
241
384
|
}
|
|
242
385
|
|
|
243
|
-
function renderCard(card, now, { active = true } = {}) {
|
|
386
|
+
function renderCard(card, now, { active = true, timeZone = undefined } = {}) {
|
|
244
387
|
const meters = Array.isArray(card.meters) ? card.meters : [];
|
|
388
|
+
const charts = Array.isArray(card.charts) ? card.charts.filter(Boolean) : [];
|
|
245
389
|
const details = Array.isArray(card.details) ? card.details : [];
|
|
246
|
-
const
|
|
390
|
+
const allNotes = Array.isArray(card.notes) ? card.notes.filter(Boolean) : [];
|
|
391
|
+
// Figures go in one list under the bars; sentences stay notes, and on a
|
|
392
|
+
// card with figures they come last, after the tables, so the numbers read
|
|
393
|
+
// first.
|
|
394
|
+
const facts = allNotes.filter(isFactNote);
|
|
395
|
+
const notes = allNotes.filter((note) => !isFactNote(note));
|
|
396
|
+
const notesHtml = notes.map((note) => renderNote(note, { now, timeZone })).join('');
|
|
397
|
+
const detailsHtml = details.map(renderDetailSection).join('');
|
|
247
398
|
const links = Array.isArray(card.links) ? card.links : [];
|
|
248
399
|
|
|
249
400
|
const statusBadge = STATUS_BADGES[card.status];
|
|
@@ -281,8 +432,9 @@ function renderCard(card, now, { active = true } = {}) {
|
|
|
281
432
|
</header>
|
|
282
433
|
${card.message ? `<div class="plan-usage-message">${escapeHtml(card.message)}</div>` : ''}
|
|
283
434
|
${metersHtml}
|
|
284
|
-
${
|
|
285
|
-
${
|
|
435
|
+
${facts.length ? `<dl class="plan-usage-facts">${facts.map(renderFact).join('')}</dl>` : ''}
|
|
436
|
+
${charts.map(renderChart).join('')}
|
|
437
|
+
${facts.length ? `${detailsHtml}${notesHtml}` : `${notesHtml}${detailsHtml}`}
|
|
286
438
|
<div class="plan-usage-card-foot">
|
|
287
439
|
${capturedLabel ? `<span class="plan-usage-captured">${capturedLabel}</span>` : ''}
|
|
288
440
|
${links.map((link) => `<a class="plan-usage-link" href="${escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(link.label)}</a>`).join('')}
|
|
@@ -306,13 +458,13 @@ function renderTab(card, activeProvider) {
|
|
|
306
458
|
* tab when that provider has a card
|
|
307
459
|
* @returns {string} modal body HTML, or '' when there is nothing to render
|
|
308
460
|
*/
|
|
309
|
-
export function renderPlanUsageHtml(report, { now = new Date(), activeProvider = '' } = {}) {
|
|
461
|
+
export function renderPlanUsageHtml(report, { now = new Date(), activeProvider = '', timeZone = undefined } = {}) {
|
|
310
462
|
const providers = Array.isArray(report?.providers) ? report.providers.filter(Boolean) : [];
|
|
311
463
|
if (!providers.length) return '';
|
|
312
464
|
const active = resolveActiveUsageProvider(providers, activeProvider);
|
|
313
465
|
const tabs = providers.map((card) => renderTab(card, active)).join('');
|
|
314
466
|
const cards = providers
|
|
315
|
-
.map((card) => renderCard(card, now, { active: normalizeUsageProvider(card.provider) === active }))
|
|
467
|
+
.map((card) => renderCard(card, now, { active: normalizeUsageProvider(card.provider) === active, timeZone }))
|
|
316
468
|
.join('');
|
|
317
469
|
return `<div class="plan-usage"><div class="plan-usage-tab-strip" role="tablist" aria-label="Usage providers">${tabs}</div>${cards}</div>`;
|
|
318
470
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Which providers are known to be ready to run a chat, from what the client
|
|
2
2
|
// already loads: the provider settings payloads and the model catalog. No DOM.
|
|
3
3
|
//
|
|
4
|
-
// OpenAI, Claude, Claude Cloud, Cursor and
|
|
5
|
-
// settings carry `enabled`. Copilot has no such flag and the relay never asks
|
|
4
|
+
// OpenAI, Claude, Claude API, Claude Cloud, Cursor, Grok and DeepSeek say so
|
|
5
|
+
// themselves: their settings carry `enabled`. Copilot has no such flag and the relay never asks
|
|
6
6
|
// whether the Copilot CLI is signed in, so the best signal is indirect: has a
|
|
7
7
|
// real Copilot model list ever reached the catalog? A fresh database is seeded
|
|
8
8
|
// with a curated list under the source `bootstrap-seed`; a server discovery,
|
|
@@ -20,9 +20,11 @@ export const SEED_CATALOG_SOURCE = 'bootstrap-seed';
|
|
|
20
20
|
const SETTINGS_PROVIDERS = Object.freeze([
|
|
21
21
|
['openai', 'openai'],
|
|
22
22
|
['claude', 'claude'],
|
|
23
|
+
['claudeApi', 'claude-api'],
|
|
23
24
|
['claudeCloud', 'claude-cloud'],
|
|
24
25
|
['cursor', 'cursor'],
|
|
25
26
|
['grok', 'grok'],
|
|
27
|
+
['deepseek', 'deepseek'],
|
|
26
28
|
]);
|
|
27
29
|
|
|
28
30
|
/** Payloads whose absence means "could not be loaded" rather than "not offered". */
|
|
@@ -32,9 +34,11 @@ const REQUIRED_INPUTS = Object.freeze(['openai', 'claude', 'cursor', 'grok', 'ca
|
|
|
32
34
|
* @param {object} input the settings payloads New Chat loads, plus GET /api/models
|
|
33
35
|
* @param {object|null} [input.openai]
|
|
34
36
|
* @param {object|null} [input.claude]
|
|
37
|
+
* @param {object|null} [input.claudeApi] null on a relay without Claude API: not offered, not unknown
|
|
35
38
|
* @param {object|null} [input.claudeCloud] null on a relay without Claude Cloud: not offered, not unknown
|
|
36
39
|
* @param {object|null} [input.cursor]
|
|
37
40
|
* @param {object|null} [input.grok]
|
|
41
|
+
* @param {object|null} [input.deepseek] null on a relay without DeepSeek: not offered, not unknown
|
|
38
42
|
* @param {object|null} [input.catalog]
|
|
39
43
|
* @returns {{ known: boolean, anyReady: boolean, ready: string[], copilot: 'ready' | 'unknown' }}
|
|
40
44
|
*/
|
|
@@ -66,9 +70,11 @@ export function isSetupRequired(readiness) {
|
|
|
66
70
|
const inputs = {
|
|
67
71
|
openai: undefined,
|
|
68
72
|
claude: undefined,
|
|
73
|
+
claudeApi: undefined,
|
|
69
74
|
claudeCloud: undefined,
|
|
70
75
|
cursor: undefined,
|
|
71
76
|
grok: undefined,
|
|
77
|
+
deepseek: undefined,
|
|
72
78
|
catalog: undefined,
|
|
73
79
|
};
|
|
74
80
|
let current = resolveProviderReadiness(inputs);
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
const CLAUDE_SETTINGS_CTA = Object.freeze([
|
|
17
17
|
Object.freeze({ action: 'open-claude-settings', label: 'Claude settings' }),
|
|
18
18
|
]);
|
|
19
|
+
const CLAUDE_API_SETTINGS_CTA = Object.freeze([
|
|
20
|
+
Object.freeze({ action: 'open-claude-api-settings', label: 'Claude API settings' }),
|
|
21
|
+
]);
|
|
22
|
+
const SWITCH_TO_SUBSCRIPTION_CTA = Object.freeze({ action: 'switch-claude-billing-to-subscription', label: 'Switch to subscription' });
|
|
23
|
+
const CLAUDE_API_UNAVAILABLE_CTA = Object.freeze([
|
|
24
|
+
Object.freeze({ action: 'open-claude-api-settings', label: 'Claude API settings' }),
|
|
25
|
+
SWITCH_TO_SUBSCRIPTION_CTA,
|
|
26
|
+
]);
|
|
19
27
|
|
|
20
28
|
const RELAY_ERROR_CTAS = Object.freeze({
|
|
21
29
|
'grok-cli-missing': Object.freeze([
|
|
@@ -36,6 +44,39 @@ const RELAY_ERROR_CTAS = Object.freeze({
|
|
|
36
44
|
'claude-account-on-hold': CLAUDE_SETTINGS_CTA,
|
|
37
45
|
'claude-verification-required': CLAUDE_SETTINGS_CTA,
|
|
38
46
|
'claude-account-access': CLAUDE_SETTINGS_CTA,
|
|
47
|
+
// A DeepSeek turn refused for its key or its balance (deepseek-claude-backend.mjs).
|
|
48
|
+
'deepseek-api-key': Object.freeze([
|
|
49
|
+
Object.freeze({ action: 'open-deepseek-settings', label: 'DeepSeek settings' }),
|
|
50
|
+
]),
|
|
51
|
+
'deepseek-balance': Object.freeze([
|
|
52
|
+
Object.freeze({ action: 'open-deepseek-top-up', label: 'Top up DeepSeek' }),
|
|
53
|
+
Object.freeze({ action: 'open-deepseek-settings', label: 'DeepSeek settings' }),
|
|
54
|
+
]),
|
|
55
|
+
// A Claude API row failed closed at the claim (server-runtime.mjs,
|
|
56
|
+
// claudeApiClaimCheck): no key or the provider switched off, or this
|
|
57
|
+
// month's cap reached. The way out is the tab (at its cap field for the
|
|
58
|
+
// cap), or the subscription, which the cap never blocks.
|
|
59
|
+
'claude-api-not-configured': CLAUDE_API_UNAVAILABLE_CTA,
|
|
60
|
+
'claude-api-cap-reached': Object.freeze([
|
|
61
|
+
Object.freeze({ action: 'open-claude-api-cap', label: 'Claude API settings' }),
|
|
62
|
+
SWITCH_TO_SUBSCRIPTION_CTA,
|
|
63
|
+
]),
|
|
64
|
+
// A turn the Claude API refused (claude-api-backend.mjs, describeClaudeApiFailure).
|
|
65
|
+
'claude-api-key': CLAUDE_API_SETTINGS_CTA,
|
|
66
|
+
'claude-api-key-source': CLAUDE_API_SETTINGS_CTA,
|
|
67
|
+
'claude-api-credit': Object.freeze([
|
|
68
|
+
Object.freeze({ action: 'open-claude-api-billing', label: 'Add funds' }),
|
|
69
|
+
Object.freeze({ action: 'open-claude-api-settings', label: 'Claude API settings' }),
|
|
70
|
+
]),
|
|
71
|
+
'claude-api-billing': Object.freeze([
|
|
72
|
+
Object.freeze({ action: 'open-claude-api-billing', label: 'Console billing' }),
|
|
73
|
+
]),
|
|
74
|
+
'claude-api-spend-limit': Object.freeze([
|
|
75
|
+
Object.freeze({ action: 'open-claude-api-limits', label: 'Console limits' }),
|
|
76
|
+
]),
|
|
77
|
+
'claude-api-rate-limit': Object.freeze([
|
|
78
|
+
Object.freeze({ action: 'open-claude-api-limits', label: 'Console limits' }),
|
|
79
|
+
]),
|
|
39
80
|
});
|
|
40
81
|
|
|
41
82
|
const STABLE_CODE_PATTERN = /error code:\s*relay\.([a-z0-9-]+)/i;
|
|
@@ -73,3 +114,20 @@ export function relayErrorCtaActionsForCode(code) {
|
|
|
73
114
|
const normalized = String(code || '').trim().toLowerCase().replace(/^relay\./, '');
|
|
74
115
|
return RELAY_ERROR_CTAS[normalized] || [];
|
|
75
116
|
}
|
|
117
|
+
|
|
118
|
+
export const START_ON_SUBSCRIPTION_ACTION = 'start-new-chat-on-subscription';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The buttons of New Chat when the relay refused to start a Claude API chat
|
|
122
|
+
* (its 409 code, CLAUDE_API_CAP_REACHED or CLAUDE_API_NOT_CONFIGURED; nothing
|
|
123
|
+
* was bound): the transcript's buttons for that code, where "Switch to
|
|
124
|
+
* subscription" becomes "Start on subscription" (the same new chat on the
|
|
125
|
+
* Claude provider), offered only when New Chat offers the Claude provider.
|
|
126
|
+
*/
|
|
127
|
+
export function newChatClaudeApiRefusalActions(code, { subscriptionOffered = false } = {}) {
|
|
128
|
+
const normalized = String(code || '').trim().toLowerCase().replace(/_/g, '-');
|
|
129
|
+
return relayErrorCtaActionsForCode(normalized).flatMap((item) => {
|
|
130
|
+
if (item.action !== 'switch-claude-billing-to-subscription') return [item];
|
|
131
|
+
return subscriptionOffered ? [{ action: START_ON_SUBSCRIPTION_ACTION, label: 'Start on subscription' }] : [];
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -164,7 +164,7 @@ export function syncRelayScheduleHeader() {
|
|
|
164
164
|
}
|
|
165
165
|
const next = nextScheduledAt(mine);
|
|
166
166
|
const nextText = next ? new Date(next).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
167
|
-
line.innerHTML = `<button type="button" class="relay-watch-chip relay-schedule-chip" aria-expanded="${panelOpen ? 'true' : 'false'}" aria-controls="relay-schedule-panel" title="Prompts the relay queues at set times (agents set them with
|
|
167
|
+
line.innerHTML = `<button type="button" class="relay-watch-chip relay-schedule-chip" aria-expanded="${panelOpen ? 'true' : 'false'}" aria-controls="relay-schedule-panel" title="Prompts the relay queues at set times (agents set them with relay_tool schedule)">⏰ ${escHtml(chipText)}${nextText ? ` <span class="relay-watch-hint">next ${escHtml(nextText)}</span>` : ''}</button>`;
|
|
168
168
|
line.hidden = false;
|
|
169
169
|
panel.innerHTML = panelOpen ? renderSchedulePanelHtml(schedules, id, { conversations, escHtml }) : '';
|
|
170
170
|
panel.hidden = !panelOpen;
|
|
@@ -55,6 +55,26 @@ import { refreshPushSettingsSection } from './push-settings.js';
|
|
|
55
55
|
import { selectSettingsTab } from './settings-tabs.js';
|
|
56
56
|
import { refreshClaudeAuthSection } from './claude-auth-ui.js';
|
|
57
57
|
import { refreshGrokAuthSection } from './grok-auth-ui.js';
|
|
58
|
+
import { syncDeepSeekSettingsInputs } from './deepseek-settings-ui.js';
|
|
59
|
+
import { syncClaudeApiSettingsInputs } from './claude-api-settings-ui.js';
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
applyDeepSeekSettingsState,
|
|
63
|
+
importDeepSeekUsageExport,
|
|
64
|
+
refreshDeepSeekSettingsState,
|
|
65
|
+
removeDeepSeekSettings,
|
|
66
|
+
saveDeepSeekSettings,
|
|
67
|
+
toggleDeepSeekProvider,
|
|
68
|
+
} from './deepseek-settings-ui.js';
|
|
69
|
+
export {
|
|
70
|
+
applyClaudeApiSettingsState,
|
|
71
|
+
openClaudeTabFromClaudeApi,
|
|
72
|
+
refreshClaudeApiModels,
|
|
73
|
+
refreshClaudeApiSettingsState,
|
|
74
|
+
removeClaudeApiSettings,
|
|
75
|
+
saveClaudeApiSettings,
|
|
76
|
+
toggleClaudeApiProvider,
|
|
77
|
+
} from './claude-api-settings-ui.js';
|
|
58
78
|
import { openClaudeCloudSettingsSection } from './claude-cloud-settings-ui.js';
|
|
59
79
|
import { refreshCliInstallSections } from './cli-install-ui.js';
|
|
60
80
|
import { refreshUpdateSection } from './update-ui.js';
|
|
@@ -1949,6 +1969,8 @@ export function openSettingsModal(tab, providerTab) {
|
|
|
1949
1969
|
cursorSettingsInputsDirty = false;
|
|
1950
1970
|
ensureCursorSettingsInputTracking();
|
|
1951
1971
|
void syncCursorSettingsInputs();
|
|
1972
|
+
void syncDeepSeekSettingsInputs();
|
|
1973
|
+
void syncClaudeApiSettingsInputs();
|
|
1952
1974
|
void refreshCursorAllowanceState();
|
|
1953
1975
|
void refreshCursorDashboardTokenState();
|
|
1954
1976
|
void refreshGrokAllowanceState();
|
|
@@ -4,7 +4,7 @@ const SETTINGS_PROVIDER_TAB_STORAGE_KEY = 'copilot_settings_provider_tab';
|
|
|
4
4
|
const SETTINGS_TABS = ['general', 'providers', 'relays', 'previews', 'shares', 'notifications', 'features'];
|
|
5
5
|
// Copilot leads: it is the default provider, and `SETTINGS_PROVIDER_TABS[0]`
|
|
6
6
|
// is also the sub-tab a first-time visitor lands on.
|
|
7
|
-
const SETTINGS_PROVIDER_TABS = ['copilot', 'openai', 'claude', 'claude-cloud', 'grok', 'cursor'];
|
|
7
|
+
const SETTINGS_PROVIDER_TABS = ['copilot', 'openai', 'claude', 'claude-api', 'claude-cloud', 'grok', 'cursor', 'deepseek'];
|
|
8
8
|
|
|
9
9
|
let settingsTabsBound = false;
|
|
10
10
|
|
|
@@ -66,7 +66,7 @@ import { updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
|
66
66
|
import { applyCliInstallState } from './cli-install-ui.js';
|
|
67
67
|
import { renderUpdateSection } from './update-ui.js';
|
|
68
68
|
import { applyHostSuspendState, applyRelayShutdownState } from './host-suspend-ui.js';
|
|
69
|
-
import { applyClaudeUsageLimit, applyUsageLimitPause } from './usage-limit-ui.js';
|
|
69
|
+
import { applyClaudeUsageLimit, applyConversationBilling, applyUsageLimitPause } from './usage-limit-ui.js';
|
|
70
70
|
import {
|
|
71
71
|
showThinking,
|
|
72
72
|
removeThinking,
|
|
@@ -153,6 +153,8 @@ let deps = null;
|
|
|
153
153
|
* @property {(payload?: object) => void} applyOpenAISettingsState
|
|
154
154
|
* @property {(payload?: object) => void} applyClaudeSettingsState
|
|
155
155
|
* @property {(payload?: object) => void} applyGrokSettingsState
|
|
156
|
+
* @property {(payload?: object) => void} applyDeepSeekSettingsState
|
|
157
|
+
* @property {(payload?: object) => void} applyClaudeApiSettingsState
|
|
156
158
|
* @property {(payload?: object) => void} applyCursorSettingsState
|
|
157
159
|
*/
|
|
158
160
|
|
|
@@ -465,6 +467,11 @@ export async function connectSocket(overrideDeps) {
|
|
|
465
467
|
});
|
|
466
468
|
// The Claude usage limit: a conversation's paused turn, and the account's
|
|
467
469
|
// latest report (also sent on connect).
|
|
470
|
+
// A Claude chat moved between the subscription and the API key, or what it
|
|
471
|
+
// may switch to changed (a swap waiting, the key removed).
|
|
472
|
+
socket.on('conversation_billing', (payload) => {
|
|
473
|
+
applyConversationBilling(payload || null);
|
|
474
|
+
});
|
|
468
475
|
socket.on('usage_limit_pause', (payload) => {
|
|
469
476
|
applyUsageLimitPause(payload || null);
|
|
470
477
|
});
|
|
@@ -497,6 +504,26 @@ export async function connectSocket(overrideDeps) {
|
|
|
497
504
|
.catch(() => {});
|
|
498
505
|
}
|
|
499
506
|
});
|
|
507
|
+
socket.on('deepseek_settings_updated', (payload) => {
|
|
508
|
+
const state = deps?.applyDeepSeekSettingsState?.(payload || {});
|
|
509
|
+
updateProviderReadinessInputs({ deepseek: state || payload || null });
|
|
510
|
+
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
511
|
+
void Promise.resolve()
|
|
512
|
+
.then(() => deps?.refreshCurrentView?.())
|
|
513
|
+
.catch(() => {});
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
// The Claude API tab saved (a key, the switch, the cap): the tab, New
|
|
517
|
+
// Chat's readiness and the composer's billing toggle follow.
|
|
518
|
+
socket.on('claude_api_settings_updated', (payload) => {
|
|
519
|
+
const state = deps?.applyClaudeApiSettingsState?.(payload || {});
|
|
520
|
+
updateProviderReadinessInputs({ claudeApi: state || payload || null });
|
|
521
|
+
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
522
|
+
void Promise.resolve()
|
|
523
|
+
.then(() => deps?.refreshCurrentView?.())
|
|
524
|
+
.catch(() => {});
|
|
525
|
+
}
|
|
526
|
+
});
|
|
500
527
|
socket.on('cursor_settings_updated', (payload) => {
|
|
501
528
|
const state = deps?.applyCursorSettingsState?.(payload || {});
|
|
502
529
|
updateProviderReadinessInputs({ cursor: state || payload || null });
|
|
@@ -31,10 +31,11 @@ export function prefixToolActivityEmoji(text) {
|
|
|
31
31
|
if (/^Tool \((create_file|write)\)/i.test(value)) return `🆕 ${value}`;
|
|
32
32
|
if (/^Tool \((bash|shell|terminal)\)/i.test(value)) return `🔧 ${value}`;
|
|
33
33
|
if (/^Tool \((sql|sqlite)\)/i.test(value)) return `🗄️ ${value}`;
|
|
34
|
-
// Work on another OAR relay. Claude's raw name is
|
|
35
|
-
// the Copilot CLI names MCP tools <server>-<tool> (oar-
|
|
34
|
+
// Work on another OAR relay. Claude's raw name is mcp__relay__relay_tool and
|
|
35
|
+
// the Copilot CLI names MCP tools <server>-<tool> (oar-relay_tool); the
|
|
36
36
|
// activity normalisers strip both, but a line that kept one matches too.
|
|
37
|
-
|
|
37
|
+
// Lines written up to 0.9.17 carry the old name, remote_relay.
|
|
38
|
+
if (/^Tool \((?:mcp__[a-z0-9_-]+__|[a-z0-9_]+-)?(?:relay_tool|remote_relay)\)/i.test(value)) return `🛰️ ${value}`;
|
|
38
39
|
if (/^Tool \(/i.test(value)) return `🛠️ ${value}`;
|
|
39
40
|
return `ℹ️ ${value}`;
|
|
40
41
|
}
|