@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The Inbox, as DOM: every pending question across this relay and its paired
|
|
2
2
|
// relays (GET /api/inbox), grouped by relay, newest first; what was answered
|
|
3
3
|
// or dismissed in the last day under a divider. And the card a question that
|
|
4
|
-
// does not block (
|
|
4
|
+
// does not block (relay_tool `ask`) shows in its own session.
|
|
5
5
|
//
|
|
6
6
|
// Every text here is written by an agent, some on other relays: it goes into
|
|
7
7
|
// the page through textContent only, never as HTML or a link. Buttons carry
|
|
@@ -72,13 +72,16 @@ import {
|
|
|
72
72
|
loadClaudeCloudBranches,
|
|
73
73
|
loadClaudeCloudRepos,
|
|
74
74
|
loadGrokSettings,
|
|
75
|
+
loadDeepSeekSettings,
|
|
76
|
+
loadClaudeApiSettings,
|
|
75
77
|
loadOpenAISettings,
|
|
76
78
|
} from './api-client.js';
|
|
77
79
|
import { isSetupRequired, updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
78
80
|
import { SETUP_TEXT } from './no-conversation-state.mjs';
|
|
79
81
|
import { openSettingsModal } from './settings-modal.js';
|
|
82
|
+
import { START_ON_SUBSCRIPTION_ACTION, newChatClaudeApiRefusalActions } from './relay-error-ctas.mjs';
|
|
80
83
|
import { captureReadingBlock, restoreReadingBlock } from './transcript-viewport-anchor.mjs';
|
|
81
|
-
import { renderMessages, renderNoConversationState, restoreInFlightThinking, focusConversationMessageById, noteTranscriptJump, getTranscriptJumpEpoch, getTranscriptLiveRowEpoch, flushConversationDraft, hydrateConversationDraft, beginConversationDraftSwitch, openStopTurnConfirmationForConversation, conversationHasActiveTurn, captureConversationReadingAnchor, restoreConversationReadingAnchor, detachConversationWindowFromTail, mergeMessagesIntoTranscript, isConversationWindowAtTail } from './conversation-view.js';
|
|
84
|
+
import { runRelayErrorCta, renderMessages, renderNoConversationState, restoreInFlightThinking, focusConversationMessageById, noteTranscriptJump, getTranscriptJumpEpoch, getTranscriptLiveRowEpoch, flushConversationDraft, hydrateConversationDraft, beginConversationDraftSwitch, openStopTurnConfirmationForConversation, conversationHasActiveTurn, captureConversationReadingAnchor, restoreConversationReadingAnchor, detachConversationWindowFromTail, mergeMessagesIntoTranscript, isConversationWindowAtTail } from './conversation-view.js';
|
|
82
85
|
import { mergeTailPage } from './transcript-reading-anchor.mjs';
|
|
83
86
|
import { readWindowRetention } from './conversation-window-retention.mjs';
|
|
84
87
|
import { MENU_SEPARATOR, bindLongPress, closeContextMenu, openContextMenu } from './context-menu.mjs';
|
|
@@ -104,6 +107,7 @@ import { createInfiniteLoader } from './infinite-loader.js';
|
|
|
104
107
|
import {
|
|
105
108
|
buildNewConversationModelChoices,
|
|
106
109
|
newConversationContextTierState,
|
|
110
|
+
preselectNewConversationModel,
|
|
107
111
|
reasoningChoicesForProviderModel,
|
|
108
112
|
resolvePreferredReasoningEffort,
|
|
109
113
|
} from './new-conversation-model-choice.mjs';
|
|
@@ -189,6 +193,8 @@ let newConversationOpenAISettingsCache = null;
|
|
|
189
193
|
let newConversationClaudeSettingsCache = null;
|
|
190
194
|
let newConversationCursorSettingsCache = null;
|
|
191
195
|
let newConversationGrokSettingsCache = null;
|
|
196
|
+
let newConversationDeepSeekSettingsCache = null;
|
|
197
|
+
let newConversationClaudeApiSettingsCache = null;
|
|
192
198
|
let newConversationClaudeCloudSettingsCache = null;
|
|
193
199
|
// Claude Cloud fields of the New Chat modal: what the selected folder's git
|
|
194
200
|
// remote said, and what was written into Repository/Branch from it (so a later
|
|
@@ -698,7 +704,7 @@ function paintConvList() {
|
|
|
698
704
|
: renderConversationOriginMarkerHtml(c, { conversations });
|
|
699
705
|
// "watched": a session another conversation watches by id (relay events).
|
|
700
706
|
const watchedMarkerHtml = relayEventWatchedMarkerHtml(c.id);
|
|
701
|
-
// "scheduled": a schedule fires into this session (
|
|
707
|
+
// "scheduled": a schedule fires into this session (relay_tool schedule).
|
|
702
708
|
const scheduledMarkerHtml = relayScheduleMarkerHtml(c.id);
|
|
703
709
|
const groupClass = item.kind === 'head' ? ' conv-group-head' : (item.kind === 'member' ? ' conv-group-member' : '');
|
|
704
710
|
const groupAttr = item.kind === 'head' || item.kind === 'member' ? ` data-group-id="${escHtml(item.group.id)}"` : '';
|
|
@@ -924,6 +930,8 @@ export function applyLoadedConversationState(id, response, {
|
|
|
924
930
|
// Absent from an older relay's payload: keep what the socket said.
|
|
925
931
|
pause: 'usageLimitPause' in response ? response.usageLimitPause : undefined,
|
|
926
932
|
providerType: response.runtimeSession?.providerType,
|
|
933
|
+
// A Claude chat's billing and whether it may switch (an older relay: absent).
|
|
934
|
+
billing: 'billing' in response ? response.billing : undefined,
|
|
927
935
|
});
|
|
928
936
|
// Merge, not replace: this payload only carries one conversation's previews.
|
|
929
937
|
mergeConversationPreviews(id, response.previews || []);
|
|
@@ -1234,9 +1242,11 @@ function normalizeNewConversationProviderType(value = '') {
|
|
|
1234
1242
|
if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
|
|
1235
1243
|
if (normalized === 'openai-image' || normalized === 'openai-image-byok') return 'openai-image';
|
|
1236
1244
|
if (normalized === 'claude') return 'claude';
|
|
1245
|
+
if (normalized === 'claude-api') return 'claude-api';
|
|
1237
1246
|
if (normalized === CLAUDE_CLOUD_PROVIDER) return CLAUDE_CLOUD_PROVIDER;
|
|
1238
1247
|
if (normalized === 'cursor') return 'cursor';
|
|
1239
1248
|
if (normalized === 'grok') return 'grok';
|
|
1249
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
1240
1250
|
return 'github';
|
|
1241
1251
|
}
|
|
1242
1252
|
|
|
@@ -1274,6 +1284,8 @@ function modelMatchesNewConversationProvider(catalog = {}, modelId = '', provide
|
|
|
1274
1284
|
const hasClaude = providers.includes('claude');
|
|
1275
1285
|
const hasCursor = providers.includes('cursor');
|
|
1276
1286
|
const hasGrok = providers.includes('grok');
|
|
1287
|
+
const hasDeepSeek = providers.includes('deepseek');
|
|
1288
|
+
const hasClaudeApi = providers.includes('claude-api');
|
|
1277
1289
|
if (wantsOpenAI) {
|
|
1278
1290
|
if (hasOpenAIByok) return true;
|
|
1279
1291
|
const settingsModel = String(newConversationOpenAISettingsCache?.model || '').trim();
|
|
@@ -1309,16 +1321,36 @@ function modelMatchesNewConversationProvider(catalog = {}, modelId = '', provide
|
|
|
1309
1321
|
: [];
|
|
1310
1322
|
return normalizedModelId === grokModel || grokModels.includes(normalizedModelId);
|
|
1311
1323
|
}
|
|
1312
|
-
|
|
1324
|
+
if (normalizedProvider === 'deepseek') {
|
|
1325
|
+
if (hasDeepSeek) return true;
|
|
1326
|
+
const deepseekModel = String(newConversationDeepSeekSettingsCache?.model || '').trim();
|
|
1327
|
+
const deepseekModels = Array.isArray(newConversationDeepSeekSettingsCache?.models)
|
|
1328
|
+
? newConversationDeepSeekSettingsCache.models.map((entry) => String(entry || '').trim()).filter(Boolean)
|
|
1329
|
+
: [];
|
|
1330
|
+
return normalizedModelId === deepseekModel || deepseekModels.includes(normalizedModelId);
|
|
1331
|
+
}
|
|
1332
|
+
if (normalizedProvider === 'claude-api') {
|
|
1333
|
+
if (hasClaudeApi) return true;
|
|
1334
|
+
const claudeApiModel = String(newConversationClaudeApiSettingsCache?.model || '').trim();
|
|
1335
|
+
const claudeApiModels = Array.isArray(newConversationClaudeApiSettingsCache?.models)
|
|
1336
|
+
? newConversationClaudeApiSettingsCache.models.map((entry) => String(entry || '').trim()).filter(Boolean)
|
|
1337
|
+
: [];
|
|
1338
|
+
return normalizedModelId === claudeApiModel || claudeApiModels.includes(normalizedModelId);
|
|
1339
|
+
}
|
|
1340
|
+
// Claude API serves the Claude provider's ids: still none of Copilot's.
|
|
1341
|
+
const claudeOnly = hasClaude && providers.every((provider) => provider === 'claude' || provider === 'claude-api');
|
|
1313
1342
|
if (claudeOnly) return false;
|
|
1314
1343
|
const cursorOnly = hasCursor && providers.every((provider) => provider === 'cursor');
|
|
1315
1344
|
if (cursorOnly) return false;
|
|
1316
1345
|
const grokOnly = hasGrok && providers.every((provider) => provider === 'grok');
|
|
1317
1346
|
if (grokOnly) return false;
|
|
1347
|
+
const deepseekOnly = hasDeepSeek && providers.every((provider) => provider === 'deepseek');
|
|
1348
|
+
if (deepseekOnly) return false;
|
|
1318
1349
|
// Should a relay ever tag catalog rows for Claude Cloud, they are no more
|
|
1319
1350
|
// Copilot's than the other runtimes' rows are.
|
|
1320
1351
|
const exclusiveOnly = providers.length > 0 && providers.every((provider) => (
|
|
1321
|
-
provider === 'claude' || provider === 'cursor' || provider === 'grok' || provider ===
|
|
1352
|
+
provider === 'claude' || provider === 'cursor' || provider === 'grok' || provider === 'deepseek'
|
|
1353
|
+
|| provider === 'claude-api' || provider === CLAUDE_CLOUD_PROVIDER
|
|
1322
1354
|
));
|
|
1323
1355
|
if (exclusiveOnly) return false;
|
|
1324
1356
|
return providers.some((provider) => (
|
|
@@ -1326,6 +1358,8 @@ function modelMatchesNewConversationProvider(catalog = {}, modelId = '', provide
|
|
|
1326
1358
|
&& provider !== 'claude'
|
|
1327
1359
|
&& provider !== 'cursor'
|
|
1328
1360
|
&& provider !== 'grok'
|
|
1361
|
+
&& provider !== 'deepseek'
|
|
1362
|
+
&& provider !== 'claude-api'
|
|
1329
1363
|
&& provider !== CLAUDE_CLOUD_PROVIDER
|
|
1330
1364
|
)) || !hasOpenAIByok;
|
|
1331
1365
|
}
|
|
@@ -1443,8 +1477,10 @@ async function populateNewConversationReasoningSelect(selectedModel = '') {
|
|
|
1443
1477
|
if (optionTitle) option.title = optionTitle;
|
|
1444
1478
|
select.appendChild(option);
|
|
1445
1479
|
}
|
|
1480
|
+
// A DeepSeek chat with nothing remembered starts at DeepSeek's own default.
|
|
1481
|
+
const storedEffort = localStorage.getItem(REASONING_STORAGE_KEY);
|
|
1446
1482
|
const preferred = resolvePreferredReasoningEffort(efforts, [
|
|
1447
|
-
|
|
1483
|
+
storedEffort || (provider === 'deepseek' ? 'high' : ''),
|
|
1448
1484
|
]);
|
|
1449
1485
|
select.value = preferred || efforts[0];
|
|
1450
1486
|
select.disabled = false;
|
|
@@ -1475,6 +1511,10 @@ function updateNewConversationProviderHelp(provider = 'github') {
|
|
|
1475
1511
|
help.textContent = "Claude chats run through the Claude Agent SDK with the relay host's Claude login.";
|
|
1476
1512
|
return;
|
|
1477
1513
|
}
|
|
1514
|
+
if (normalizedProvider === 'claude-api') {
|
|
1515
|
+
help.textContent = 'Claude API chats run through the Claude Agent SDK on the API key saved in Settings, billed to its Claude Console organisation. The chat can switch to the subscription later.';
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1478
1518
|
if (normalizedProvider === CLAUDE_CLOUD_PROVIDER) {
|
|
1479
1519
|
help.textContent = "Claude Cloud chats run in a sandbox at Anthropic on a clone of a GitHub repository, billed to the relay host's Claude account.";
|
|
1480
1520
|
return;
|
|
@@ -1487,6 +1527,10 @@ function updateNewConversationProviderHelp(provider = 'github') {
|
|
|
1487
1527
|
help.textContent = "Grok chats use the host machine's logged-in Grok credentials (grok login or XAI_API_KEY).";
|
|
1488
1528
|
return;
|
|
1489
1529
|
}
|
|
1530
|
+
if (normalizedProvider === 'deepseek') {
|
|
1531
|
+
help.textContent = "DeepSeek chats run Claude Code on the relay host against DeepSeek's API, billed to your saved DeepSeek key.";
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1490
1534
|
help.textContent = 'Copilot models use your GitHub Copilot runtime.';
|
|
1491
1535
|
}
|
|
1492
1536
|
|
|
@@ -1516,7 +1560,7 @@ async function populateNewConversationModelSelect(providerType = 'github') {
|
|
|
1516
1560
|
buildCatalogModelOptions(eligibleModels, {
|
|
1517
1561
|
metadataByModel: catalog.modelMetadataByModel || {},
|
|
1518
1562
|
// The merged metadata knows Copilot's windows, not Claude's or the cloud's.
|
|
1519
|
-
annotateContextWindow: normalizedProvider !== 'claude' && !isCloud,
|
|
1563
|
+
annotateContextWindow: normalizedProvider !== 'claude' && normalizedProvider !== 'claude-api' && !isCloud,
|
|
1520
1564
|
}).filter((option) => hasAuto || option.value.toLowerCase() !== 'auto'),
|
|
1521
1565
|
);
|
|
1522
1566
|
for (const choice of choices) {
|
|
@@ -1534,10 +1578,13 @@ async function populateNewConversationModelSelect(providerType = 'github') {
|
|
|
1534
1578
|
// The tab's default, not the last model another provider's chat used.
|
|
1535
1579
|
const cloudDefault = claudeCloudDefaultModel(newConversationClaudeCloudSettingsCache, eligibleModels);
|
|
1536
1580
|
if (cloudDefault) target.value = cloudDefault;
|
|
1537
|
-
} else
|
|
1538
|
-
target.value
|
|
1539
|
-
|
|
1540
|
-
|
|
1581
|
+
} else {
|
|
1582
|
+
const preselected = preselectNewConversationModel(Array.from(target.options, (option) => option.value), {
|
|
1583
|
+
storedModel,
|
|
1584
|
+
providerDefault: normalizedProvider === 'claude-api' ? newConversationClaudeApiSettingsCache?.model : '',
|
|
1585
|
+
allowAuto: normalizedProvider !== 'openai-image',
|
|
1586
|
+
});
|
|
1587
|
+
if (preselected) target.value = preselected;
|
|
1541
1588
|
}
|
|
1542
1589
|
updateNewConversationProviderHelp(normalizedProvider);
|
|
1543
1590
|
syncNewConversationReasoningLabel(normalizedProvider);
|
|
@@ -2094,7 +2141,7 @@ function applyNewConversationSetupState(setupRequired) {
|
|
|
2094
2141
|
}
|
|
2095
2142
|
|
|
2096
2143
|
async function openNewConversationModelModal() {
|
|
2097
|
-
const [catalog, settings, claudeSettings, cursorSettings, grokSettings, claudeCloudSettings] = await Promise.all([
|
|
2144
|
+
const [catalog, settings, claudeSettings, cursorSettings, grokSettings, claudeCloudSettings, deepseekSettings, claudeApiSettings] = await Promise.all([
|
|
2098
2145
|
loadModelCatalog(),
|
|
2099
2146
|
loadOpenAISettings(),
|
|
2100
2147
|
loadClaudeSettings(),
|
|
@@ -2102,6 +2149,10 @@ async function openNewConversationModelModal() {
|
|
|
2102
2149
|
loadGrokSettings(),
|
|
2103
2150
|
// null on a relay without Claude Cloud: the option is simply not offered.
|
|
2104
2151
|
loadClaudeCloudSettings(),
|
|
2152
|
+
// The same for a relay without DeepSeek.
|
|
2153
|
+
loadDeepSeekSettings(),
|
|
2154
|
+
// And without Claude API.
|
|
2155
|
+
loadClaudeApiSettings(),
|
|
2105
2156
|
]);
|
|
2106
2157
|
newConversationCatalogCache = catalog || null;
|
|
2107
2158
|
newConversationOpenAISettingsCache = settings || null;
|
|
@@ -2109,6 +2160,8 @@ async function openNewConversationModelModal() {
|
|
|
2109
2160
|
newConversationCursorSettingsCache = cursorSettings || null;
|
|
2110
2161
|
newConversationGrokSettingsCache = grokSettings || null;
|
|
2111
2162
|
newConversationClaudeCloudSettingsCache = claudeCloudSettings || null;
|
|
2163
|
+
newConversationDeepSeekSettingsCache = deepseekSettings || null;
|
|
2164
|
+
newConversationClaudeApiSettingsCache = claudeApiSettings || null;
|
|
2112
2165
|
// Fresh answers for the readiness question, shared with the empty screen
|
|
2113
2166
|
// and the composer. With no provider known to be ready the modal opens in
|
|
2114
2167
|
// its setup state: Settings is the main button, Start stays as "anyway".
|
|
@@ -2118,11 +2171,14 @@ async function openNewConversationModelModal() {
|
|
|
2118
2171
|
claudeCloud: claudeCloudSettings || null,
|
|
2119
2172
|
cursor: cursorSettings || null,
|
|
2120
2173
|
grok: grokSettings || null,
|
|
2174
|
+
deepseek: deepseekSettings || null,
|
|
2175
|
+
claudeApi: claudeApiSettings || null,
|
|
2121
2176
|
catalog: catalog || null,
|
|
2122
2177
|
});
|
|
2123
2178
|
applyNewConversationSetupState(isSetupRequired(readiness));
|
|
2124
2179
|
// Every open starts from empty cloud fields; the folder lookup refills them.
|
|
2125
2180
|
resetNewConversationCloudState();
|
|
2181
|
+
clearNewConversationClaudeApiRefusal();
|
|
2126
2182
|
bindNewConversationCloudInputs();
|
|
2127
2183
|
const providerSelect = document.getElementById('new-conversation-provider-select');
|
|
2128
2184
|
if (providerSelect) {
|
|
@@ -2134,6 +2190,9 @@ async function openNewConversationModelModal() {
|
|
|
2134
2190
|
if (claudeSettings?.enabled === true) {
|
|
2135
2191
|
options.push({ value: 'claude', label: 'Claude SDK' });
|
|
2136
2192
|
}
|
|
2193
|
+
if (claudeApiSettings?.enabled === true) {
|
|
2194
|
+
options.push({ value: 'claude-api', label: 'Claude API' });
|
|
2195
|
+
}
|
|
2137
2196
|
if (claudeCloudSettings?.enabled === true) {
|
|
2138
2197
|
options.push({ value: CLAUDE_CLOUD_PROVIDER, label: 'Claude Cloud' });
|
|
2139
2198
|
}
|
|
@@ -2143,6 +2202,9 @@ async function openNewConversationModelModal() {
|
|
|
2143
2202
|
if (grokSettings?.enabled === true) {
|
|
2144
2203
|
options.push({ value: 'grok', label: 'Grok' });
|
|
2145
2204
|
}
|
|
2205
|
+
if (deepseekSettings?.enabled === true) {
|
|
2206
|
+
options.push({ value: 'deepseek', label: 'DeepSeek' });
|
|
2207
|
+
}
|
|
2146
2208
|
providerSelect.innerHTML = '';
|
|
2147
2209
|
for (const option of options) {
|
|
2148
2210
|
const entry = document.createElement('option');
|
|
@@ -2160,6 +2222,7 @@ async function openNewConversationModelModal() {
|
|
|
2160
2222
|
if (providerSelect.dataset.modelsBound !== '1') {
|
|
2161
2223
|
providerSelect.dataset.modelsBound = '1';
|
|
2162
2224
|
providerSelect.addEventListener('change', () => {
|
|
2225
|
+
clearNewConversationClaudeApiRefusal();
|
|
2163
2226
|
void populateNewConversationModelSelect(providerSelect.value);
|
|
2164
2227
|
});
|
|
2165
2228
|
}
|
|
@@ -2348,6 +2411,18 @@ async function createNewConversation(selectedModel, selectedReasoningEffort = ''
|
|
|
2348
2411
|
showNewConversationCloudError(cloudError.text, cloudError.field ? [cloudError.field] : []);
|
|
2349
2412
|
return;
|
|
2350
2413
|
}
|
|
2414
|
+
// Claude API refused to start (this month's cap, no key, switched off):
|
|
2415
|
+
// nothing was bound. The relay's words stay in the modal, with the way
|
|
2416
|
+
// to fix it and the same chat on the subscription.
|
|
2417
|
+
const claudeApiActions = !recovered && selectedProvider === 'claude-api'
|
|
2418
|
+
? newChatClaudeApiRefusalActions(error?.payload?.code, {
|
|
2419
|
+
subscriptionOffered: newConversationProviderOffered('claude'),
|
|
2420
|
+
})
|
|
2421
|
+
: [];
|
|
2422
|
+
if (claudeApiActions.length) {
|
|
2423
|
+
showNewConversationClaudeApiRefusal(error?.message || 'Claude API could not start this chat.', claudeApiActions, selectedModel);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2351
2426
|
showTransientRelayNotice(recovered
|
|
2352
2427
|
// The chat is usable, but its worker did not start, so say what broke.
|
|
2353
2428
|
? `The chat was created, but its session could not start: ${error?.message || 'unknown error'}.`
|
|
@@ -2359,8 +2434,78 @@ async function createNewConversation(selectedModel, selectedReasoningEffort = ''
|
|
|
2359
2434
|
}
|
|
2360
2435
|
}
|
|
2361
2436
|
|
|
2437
|
+
function newConversationProviderOffered(provider) {
|
|
2438
|
+
return Array.from(document.getElementById('new-conversation-provider-select')?.options || [])
|
|
2439
|
+
.some((option) => option.value === provider);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function clearNewConversationClaudeApiRefusal() {
|
|
2443
|
+
const note = document.getElementById('new-conversation-refusal');
|
|
2444
|
+
if (!note) return;
|
|
2445
|
+
note.replaceChildren();
|
|
2446
|
+
note.hidden = true;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// The same chat, started on the Claude provider: its model when the Claude
|
|
2450
|
+
// list has it, the choices the modal holds otherwise.
|
|
2451
|
+
async function startNewConversationOnSubscription(model) {
|
|
2452
|
+
const providerSelect = document.getElementById('new-conversation-provider-select');
|
|
2453
|
+
if (!providerSelect || newConversationInFlight) return;
|
|
2454
|
+
clearNewConversationClaudeApiRefusal();
|
|
2455
|
+
// The effort chosen for the refused chat: rebuilding the lists for Claude
|
|
2456
|
+
// would fall back to the stored one.
|
|
2457
|
+
const effort = String(document.getElementById('new-conversation-reasoning-select')?.value || '').trim().toLowerCase();
|
|
2458
|
+
providerSelect.value = 'claude';
|
|
2459
|
+
await populateNewConversationModelSelect('claude');
|
|
2460
|
+
const modelSelect = document.getElementById('new-conversation-model-select');
|
|
2461
|
+
if (modelSelect && Array.from(modelSelect.options).some((option) => option.value === model)) {
|
|
2462
|
+
modelSelect.value = model;
|
|
2463
|
+
await populateNewConversationReasoningSelect(model);
|
|
2464
|
+
}
|
|
2465
|
+
const reasoningSelect = document.getElementById('new-conversation-reasoning-select');
|
|
2466
|
+
if (effort && reasoningSelect && Array.from(reasoningSelect.options).some((option) => option.value === effort)) {
|
|
2467
|
+
reasoningSelect.value = effort;
|
|
2468
|
+
}
|
|
2469
|
+
await confirmNewConversationModel();
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
function showNewConversationClaudeApiRefusal(message, actions, model) {
|
|
2473
|
+
const note = document.getElementById('new-conversation-refusal');
|
|
2474
|
+
if (!note) {
|
|
2475
|
+
showTransientRelayNotice(message, 12000);
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const text = document.createElement('div');
|
|
2479
|
+
text.className = 'new-conversation-refusal-text';
|
|
2480
|
+
text.textContent = message;
|
|
2481
|
+
const buttons = document.createElement('div');
|
|
2482
|
+
buttons.className = 'new-conversation-refusal-actions';
|
|
2483
|
+
for (const item of actions) {
|
|
2484
|
+
const button = document.createElement('button');
|
|
2485
|
+
button.type = 'button';
|
|
2486
|
+
button.className = 'summary-btn';
|
|
2487
|
+
button.dataset.action = item.action;
|
|
2488
|
+
button.textContent = item.label;
|
|
2489
|
+
button.addEventListener('click', (event) => {
|
|
2490
|
+
event.preventDefault();
|
|
2491
|
+
if (item.action === START_ON_SUBSCRIPTION_ACTION) {
|
|
2492
|
+
void startNewConversationOnSubscription(model);
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
closeNewConversationModelModal();
|
|
2496
|
+
runRelayErrorCta(item.action);
|
|
2497
|
+
});
|
|
2498
|
+
buttons.appendChild(button);
|
|
2499
|
+
}
|
|
2500
|
+
note.replaceChildren(text, buttons);
|
|
2501
|
+
note.hidden = false;
|
|
2502
|
+
// The modal's body scrolls: on a phone the note lands below the fold.
|
|
2503
|
+
note.scrollIntoView?.({ block: 'nearest' });
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2362
2506
|
export async function confirmNewConversationModel() {
|
|
2363
2507
|
if (newConversationInFlight) return;
|
|
2508
|
+
clearNewConversationClaudeApiRefusal();
|
|
2364
2509
|
const selectedModel = String(document.getElementById('new-conversation-model-select')?.value || '').trim();
|
|
2365
2510
|
const selectedReasoningEffort = String(document.getElementById('new-conversation-reasoning-select')?.value || '').trim().toLowerCase();
|
|
2366
2511
|
if (!selectedModel) return;
|
|
@@ -12,15 +12,17 @@ export function composerPlaceholderFor({ modelId = '', providerType = '' } = {})
|
|
|
12
12
|
const provider = String(providerType || '').trim().toLowerCase();
|
|
13
13
|
const id = String(modelId || '').trim().toLowerCase().replace(/\[[^\]]*\]$/, '');
|
|
14
14
|
const providerFallback = () => {
|
|
15
|
-
if (provider === 'claude' || provider === 'claude-cloud') return 'Message Claude…';
|
|
15
|
+
if (provider === 'claude' || provider === 'claude-api' || provider === 'claude-cloud') return 'Message Claude…';
|
|
16
16
|
if (provider === 'grok') return 'Message Grok…';
|
|
17
17
|
if (provider === 'openai' || provider === 'openai-byok') return 'Message OpenAI…';
|
|
18
18
|
if (provider === 'cursor') return 'Message Cursor…';
|
|
19
|
+
if (provider === 'deepseek') return 'Message DeepSeek…';
|
|
19
20
|
return 'Message Copilot…';
|
|
20
21
|
};
|
|
21
22
|
if (!id || id === 'auto') return providerFallback();
|
|
22
23
|
if (id.startsWith('claude-')) return 'Message Claude…';
|
|
23
24
|
if (id.startsWith('grok-')) return 'Message Grok…';
|
|
25
|
+
if (id.startsWith('deepseek-')) return 'Message DeepSeek…';
|
|
24
26
|
if (id.startsWith('gpt-')) return 'Message GPT…';
|
|
25
27
|
if (id.startsWith('gemini-')) return 'Message Gemini…';
|
|
26
28
|
// Composer is Cursor's house model family.
|
|
@@ -65,6 +67,15 @@ export function humanizeModelLabel(modelId = '') {
|
|
|
65
67
|
const label = words.map((word) => word.text).join(' ');
|
|
66
68
|
return suffixMatch ? `${label} [${suffixMatch[1]}]` : label;
|
|
67
69
|
}
|
|
70
|
+
// "deepseek-flash" → "DeepSeek Flash", "deepseek-v4-pro" → "DeepSeek V4 Pro".
|
|
71
|
+
if (/^deepseek-/i.test(text)) {
|
|
72
|
+
return `DeepSeek ${text
|
|
73
|
+
.replace(/^deepseek-/i, '')
|
|
74
|
+
.split('-')
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map((part) => (/^v\d/i.test(part) ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
77
|
+
.join(' ')}`;
|
|
78
|
+
}
|
|
68
79
|
if (/^gemini-/i.test(text)) {
|
|
69
80
|
return text
|
|
70
81
|
.replace(/^gemini-/i, 'Gemini ')
|
|
@@ -184,6 +195,22 @@ export function normalizeModelSelectorOptions(models = [], {
|
|
|
184
195
|
});
|
|
185
196
|
}
|
|
186
197
|
|
|
198
|
+
/**
|
|
199
|
+
* `humanizeModelLabel` for a list of ids, where two ids that would read the
|
|
200
|
+
* same (two dated snapshots of one model) keep their ids instead: a Map of
|
|
201
|
+
* id → label.
|
|
202
|
+
*/
|
|
203
|
+
export function distinctModelLabels(modelIds = []) {
|
|
204
|
+
const ids = (Array.isArray(modelIds) ? modelIds : []).map((id) => String(id || '').trim()).filter(Boolean);
|
|
205
|
+
const labels = new Map(ids.map((id) => [id, humanizeModelLabel(id) || id]));
|
|
206
|
+
const counts = new Map();
|
|
207
|
+
for (const label of labels.values()) counts.set(label, (counts.get(label) || 0) + 1);
|
|
208
|
+
for (const [id, label] of labels) {
|
|
209
|
+
if (counts.get(label) > 1) labels.set(id, id);
|
|
210
|
+
}
|
|
211
|
+
return labels;
|
|
212
|
+
}
|
|
213
|
+
|
|
187
214
|
/**
|
|
188
215
|
* The one option list both pickers render (composer #model-select and the
|
|
189
216
|
* New Chat modal), so they cannot disagree on order or wording. Callers filter
|
|
@@ -25,6 +25,27 @@ export function buildNewConversationModelChoices(options = []) {
|
|
|
25
25
|
return choices;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The model New Chat starts on, from the values its list offers: the
|
|
30
|
+
* composer's last model when the list has it, else the provider's own
|
|
31
|
+
* default (Claude API's settings model; the list is sorted by name, so its
|
|
32
|
+
* first entry is no default), else Auto when allowed, else ''. '' leaves the
|
|
33
|
+
* list's first entry.
|
|
34
|
+
*/
|
|
35
|
+
export function preselectNewConversationModel(values = [], {
|
|
36
|
+
storedModel = '',
|
|
37
|
+
providerDefault = '',
|
|
38
|
+
allowAuto = true,
|
|
39
|
+
} = {}) {
|
|
40
|
+
const offered = new Set((Array.isArray(values) ? values : []).map((value) => String(value || '').trim()).filter(Boolean));
|
|
41
|
+
const stored = String(storedModel || '').trim();
|
|
42
|
+
if (stored && offered.has(stored)) return stored;
|
|
43
|
+
const fallback = String(providerDefault || '').trim();
|
|
44
|
+
if (fallback && offered.has(fallback)) return fallback;
|
|
45
|
+
if (allowAuto && offered.has('auto')) return 'auto';
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
function normalizeProviderKey(provider = '') {
|
|
29
50
|
const key = String(provider || '').trim().toLowerCase();
|
|
30
51
|
if (key === 'openai-byok') return 'openai';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { isSetupRequired } from './provider-readiness.mjs';
|
|
11
11
|
|
|
12
12
|
export const SETUP_HEADING = 'Set up a provider';
|
|
13
|
-
export const SETUP_TEXT = 'Turn on Claude, Cursor, OpenAI or
|
|
13
|
+
export const SETUP_TEXT = 'Turn on Claude, Cursor, OpenAI, Grok or DeepSeek in Settings → Providers, or sign in to the Copilot CLI on the relay host.';
|
|
14
14
|
|
|
15
15
|
const ACTIONS = Object.freeze({
|
|
16
16
|
openSettings: (primary) => ({ id: 'empty-state-open-settings', action: 'open-settings', label: primary ? 'Open Settings' : 'Settings', primary, link: !primary }),
|