@oar-sh/oar 0.9.15 → 0.9.17
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/CHANGELOG.md +305 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +131 -33
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
- package/server/db-schema.mjs +115 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
- package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
- package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
- package/server/migrations/0022-relay-schedules.mjs +142 -0
- package/server/migrations/0023-relay-asks.mjs +141 -0
- 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/public/app/api-client.js +53 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/bootstrap.js +162 -16
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +17 -0
- package/server/public/app/conversation-group-order.mjs +35 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +367 -0
- package/server/public/app/inbox-view.mjs +370 -0
- package/server/public/app/journal-view.js +54 -5
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -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 +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/relay-event-subscriptions-ui.js +4 -2
- package/server/public/app/relay-event-view.mjs +33 -7
- package/server/public/app/relay-schedule-view.mjs +119 -0
- package/server/public/app/relay-schedules-ui.js +172 -0
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +37 -3
- package/server/public/app/store.js +18 -5
- package/server/public/index.html +567 -35
- package/server/relay-tools.md +18 -3
- package/server/repositories/conversation-group-repository.mjs +110 -17
- package/server/repositories/remote-relay-repository.mjs +20 -2
- package/server/repositories/session-repository.mjs +7 -1
- package/server/routes/ask-user-routes.mjs +57 -3
- package/server/routes/cache-routes.mjs +6 -0
- package/server/routes/conversation-group-routes.mjs +19 -3
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +195 -18
- package/server/routes/relay-ask-routes.mjs +387 -0
- package/server/routes/relay-event-routes.mjs +3 -1
- package/server/routes/relay-schedule-routes.mjs +57 -0
- package/server/routes/sessions-routes.mjs +932 -97
- package/server/server-runtime.mjs +203 -11
- package/server/services/about-service.mjs +1 -0
- package/server/services/cache-rebuild-service.mjs +19 -2
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/conversation-preferences-service.mjs +17 -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-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/relay-ask-link-service.mjs +174 -0
- package/server/services/relay-ask-service.mjs +604 -0
- package/server/services/relay-event-service.mjs +589 -73
- package/server/services/relay-schedule-service.mjs +596 -0
- package/server/services/remote-relay-caller-context.mjs +89 -20
- package/server/services/remote-relay-dispatcher.mjs +958 -31
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -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 +2 -2
- package/shared/relay-asks.mjs +145 -0
- package/shared/relay-events.mjs +268 -18
- package/shared/relay-schedules.mjs +128 -0
- package/shared/remote-relay-contract.mjs +554 -26
- package/shared/remote-relay-tool-core.mjs +16 -5
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
|
@@ -530,6 +530,59 @@ export async function updateCursorSettings({
|
|
|
530
530
|
return settingsRequest('/api/settings/cursor', payload, 'Failed to update Cursor settings');
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
export async function loadDeepSeekSettings() {
|
|
534
|
+
return apiFetch('/api/settings/deepseek');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Saves what the caller names: a key (written, never read back), the switch,
|
|
539
|
+
* the default model, the enabled models, the base URL (null = DeepSeek's
|
|
540
|
+
* own), the attribution and the monthly budget (null = none); `remove`
|
|
541
|
+
* takes the key out.
|
|
542
|
+
*/
|
|
543
|
+
export async function updateDeepSeekSettings({
|
|
544
|
+
apiKey = '',
|
|
545
|
+
model = undefined,
|
|
546
|
+
enabled = undefined,
|
|
547
|
+
enabledModels = undefined,
|
|
548
|
+
baseUrl = undefined,
|
|
549
|
+
attribution = undefined,
|
|
550
|
+
monthlyBudget = undefined,
|
|
551
|
+
monthlyBudgetCurrency = undefined,
|
|
552
|
+
startingBalance = undefined,
|
|
553
|
+
startingBalanceEdited = undefined,
|
|
554
|
+
remove = false,
|
|
555
|
+
} = {}) {
|
|
556
|
+
// Only the fields named here reach the relay: a field the DeepSeek tab
|
|
557
|
+
// shows needs its line below (api-client.deepseek-settings.test.mjs).
|
|
558
|
+
const payload = remove === true ? { remove: true } : {};
|
|
559
|
+
if (!payload.remove) {
|
|
560
|
+
const key = String(apiKey || '').trim();
|
|
561
|
+
if (key) payload.apiKey = key;
|
|
562
|
+
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
563
|
+
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
564
|
+
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
565
|
+
if (baseUrl === null || typeof baseUrl === 'string') payload.baseUrl = baseUrl === null ? null : String(baseUrl).trim();
|
|
566
|
+
if (typeof attribution === 'string' && attribution.trim()) payload.attribution = attribution.trim();
|
|
567
|
+
if (monthlyBudget === null || (typeof monthlyBudget === 'number' && Number.isFinite(monthlyBudget))) payload.monthlyBudget = monthlyBudget;
|
|
568
|
+
// 'USD' or 'CNY'; null is the balance's currency.
|
|
569
|
+
if (monthlyBudgetCurrency === null || typeof monthlyBudgetCurrency === 'string') {
|
|
570
|
+
payload.monthlyBudgetCurrency = String(monthlyBudgetCurrency ?? '').trim() || null;
|
|
571
|
+
}
|
|
572
|
+
if (startingBalance === null || (typeof startingBalance === 'number' && Number.isFinite(startingBalance))) payload.startingBalance = startingBalance;
|
|
573
|
+
if (typeof startingBalanceEdited === 'boolean') payload.startingBalanceEdited = startingBalanceEdited;
|
|
574
|
+
}
|
|
575
|
+
return settingsRequest('/api/settings/deepseek', payload, 'Failed to update DeepSeek settings');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Imports DeepSeek's usage export: the texts of its amount and cost files.
|
|
580
|
+
* Answers `{ ok, summary }`; a refused export throws with the relay's reason.
|
|
581
|
+
*/
|
|
582
|
+
export async function importDeepSeekUsageExport({ amount, cost }) {
|
|
583
|
+
return settingsRequest('/api/deepseek-usage-import', { amount, cost }, 'Failed to import the DeepSeek usage export');
|
|
584
|
+
}
|
|
585
|
+
|
|
533
586
|
export async function loadWindowsAutostartSetting() {
|
|
534
587
|
return apiFetchOrThrow('/api/settings/windows-autostart');
|
|
535
588
|
}
|
|
@@ -16,6 +16,7 @@ import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
|
16
16
|
import { questionCardHeadLabel } from './conversation-provider-indicator.mjs';
|
|
17
17
|
import { questionAcceptsFreeText } from './question-answer-rule.mjs';
|
|
18
18
|
import { beginOwnSend, followsTranscriptEnd, refreshJumpButton, scrollAfterOwnSend } from './transcript-jump-bottom-view.mjs';
|
|
19
|
+
import { renderSessionAskCards } from './inbox-ui.js';
|
|
19
20
|
|
|
20
21
|
let relayQuestionRenderHash = '';
|
|
21
22
|
let renderedRelayQuestionIds = new Set();
|
|
@@ -215,6 +216,8 @@ function scrollQuestionCardIntoMessages(el, target) {
|
|
|
215
216
|
export function renderRelayQuestions() {
|
|
216
217
|
const el = document.getElementById('messages');
|
|
217
218
|
if (!el) return;
|
|
219
|
+
// The questions that don't block go first: the cards a turn waits on stay last.
|
|
220
|
+
renderSessionAskCards();
|
|
218
221
|
const previousScrollTop = el.scrollTop;
|
|
219
222
|
// A card is shown to a reader who is at the end. Anyone further up stays
|
|
220
223
|
// where they are; the jump-to-bottom button says that a question waits.
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
updateClaudeSettings,
|
|
80
80
|
updateCursorSettings,
|
|
81
81
|
updateGrokSettings,
|
|
82
|
+
updateDeepSeekSettings,
|
|
82
83
|
loadConversation,
|
|
83
84
|
refreshConversationHistory,
|
|
84
85
|
updateConversationTitle,
|
|
@@ -238,6 +239,7 @@ import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
|
238
239
|
import {
|
|
239
240
|
autoCompactWindowNotes,
|
|
240
241
|
renderAutoCompactControlHtml,
|
|
242
|
+
renderAgentSessionsControlHtml,
|
|
241
243
|
renderAutoCompactNotesHtml,
|
|
242
244
|
renderContextUsageHtml,
|
|
243
245
|
renderThinkingControlHtml,
|
|
@@ -322,6 +324,12 @@ import {
|
|
|
322
324
|
toggleGrokProvider,
|
|
323
325
|
applyGrokSettingsState,
|
|
324
326
|
refreshGrokSettingsState,
|
|
327
|
+
saveDeepSeekSettings,
|
|
328
|
+
removeDeepSeekSettings,
|
|
329
|
+
toggleDeepSeekProvider,
|
|
330
|
+
importDeepSeekUsageExport,
|
|
331
|
+
applyDeepSeekSettingsState,
|
|
332
|
+
refreshDeepSeekSettingsState,
|
|
325
333
|
saveCursorSettings,
|
|
326
334
|
removeCursorSettings,
|
|
327
335
|
saveCursorAllowanceSettings,
|
|
@@ -401,6 +409,8 @@ import { openStopSharingConfirmation } from './shared-conversations-settings.js'
|
|
|
401
409
|
import { showAbout, syncAboutMenuLabel } from './about-ui.js';
|
|
402
410
|
import { syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
403
411
|
import { loadRelayEventSubscriptions, onRelayEventSubscriptionsChanged, syncRelayEventHeader } from './relay-event-subscriptions-ui.js';
|
|
412
|
+
import { loadRelaySchedules, onRelaySchedulesChanged, syncRelayScheduleHeader } from './relay-schedules-ui.js';
|
|
413
|
+
import { leaveInboxView, startInbox, toggleInboxView } from './inbox-ui.js';
|
|
404
414
|
import {
|
|
405
415
|
previewAgentSessionsMaxWait,
|
|
406
416
|
saveAgentSessionsMaxWait,
|
|
@@ -442,6 +452,7 @@ const PROVIDER_LABELS = {
|
|
|
442
452
|
'claude-cloud': 'Claude Cloud',
|
|
443
453
|
cursor: 'Cursor SDK',
|
|
444
454
|
grok: 'Grok',
|
|
455
|
+
deepseek: 'DeepSeek',
|
|
445
456
|
'github-copilot': 'GitHub Copilot',
|
|
446
457
|
// Vendor grouping for Copilot-served rows, distinct from the Claude SDK runtime.
|
|
447
458
|
anthropic: 'Anthropic',
|
|
@@ -1502,6 +1513,7 @@ function normalizeModelSelectorProviderType(providerType = '') {
|
|
|
1502
1513
|
if (normalized === CLAUDE_CLOUD_PROVIDER) return CLAUDE_CLOUD_PROVIDER;
|
|
1503
1514
|
if (normalized === 'cursor') return 'cursor';
|
|
1504
1515
|
if (normalized === 'grok') return 'grok';
|
|
1516
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
1505
1517
|
return 'github';
|
|
1506
1518
|
}
|
|
1507
1519
|
|
|
@@ -1529,6 +1541,7 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
|
|
|
1529
1541
|
const hasClaude = providers.includes('claude');
|
|
1530
1542
|
const hasCursor = providers.includes('cursor');
|
|
1531
1543
|
const hasGrok = providers.includes('grok');
|
|
1544
|
+
const hasDeepSeek = providers.includes('deepseek');
|
|
1532
1545
|
// Only present if a relay tags catalog rows for Claude Cloud; such a row is
|
|
1533
1546
|
// then as exclusive as the other runtimes' rows.
|
|
1534
1547
|
const hasClaudeCloud = providers.includes(CLAUDE_CLOUD_PROVIDER);
|
|
@@ -1537,6 +1550,7 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
|
|
|
1537
1550
|
&& provider !== 'claude'
|
|
1538
1551
|
&& provider !== 'cursor'
|
|
1539
1552
|
&& provider !== 'grok'
|
|
1553
|
+
&& provider !== 'deepseek'
|
|
1540
1554
|
&& provider !== CLAUDE_CLOUD_PROVIDER
|
|
1541
1555
|
));
|
|
1542
1556
|
const activeProvider = normalizeModelSelectorProviderType(activeProviderType);
|
|
@@ -1544,7 +1558,8 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
|
|
|
1544
1558
|
if (activeProvider === 'claude') return hasClaude;
|
|
1545
1559
|
if (activeProvider === 'cursor') return hasCursor;
|
|
1546
1560
|
if (activeProvider === 'grok') return hasGrok;
|
|
1547
|
-
|
|
1561
|
+
if (activeProvider === 'deepseek') return hasDeepSeek;
|
|
1562
|
+
return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasDeepSeek || hasClaudeCloud) && !hasNonExclusiveProvider);
|
|
1548
1563
|
}
|
|
1549
1564
|
|
|
1550
1565
|
// A Claude Cloud conversation does not pick from the shared catalog: its
|
|
@@ -1987,6 +2002,7 @@ const RELAY_MODES_BY_PROVIDER = {
|
|
|
1987
2002
|
claude: ['agent', 'ask', 'plan', 'autopilot'],
|
|
1988
2003
|
cursor: ['agent', 'ask', 'plan', 'autopilot'],
|
|
1989
2004
|
grok: ['agent', 'ask', 'plan', 'autopilot'],
|
|
2005
|
+
deepseek: ['agent', 'ask', 'plan', 'autopilot'],
|
|
1990
2006
|
'claude-cloud': ['agent'],
|
|
1991
2007
|
};
|
|
1992
2008
|
|
|
@@ -2333,6 +2349,13 @@ function reportOpenAIModelDiscoveryFailure(payload) {
|
|
|
2333
2349
|
8000,
|
|
2334
2350
|
);
|
|
2335
2351
|
}
|
|
2352
|
+
const deepseekDiscovery = payload?.deepseekModelDiscovery;
|
|
2353
|
+
if (deepseekDiscovery && deepseekDiscovery.ok === false && !deepseekDiscovery.skipped) {
|
|
2354
|
+
showTransientRelayNotice(
|
|
2355
|
+
`DeepSeek model discovery failed. Cached DeepSeek models were kept: ${deepseekDiscovery.error || 'unknown error'}`,
|
|
2356
|
+
8000,
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2336
2359
|
}
|
|
2337
2360
|
|
|
2338
2361
|
async function retryModelMetadataRefresh() {
|
|
@@ -2470,6 +2493,17 @@ function applyModelVariantCatalogState(payload) {
|
|
|
2470
2493
|
: [],
|
|
2471
2494
|
}
|
|
2472
2495
|
: null,
|
|
2496
|
+
deepseekModels: payload?.deepseekModels && typeof payload.deepseekModels === 'object'
|
|
2497
|
+
? {
|
|
2498
|
+
defaultModel: String(payload.deepseekModels.defaultModel || '').trim(),
|
|
2499
|
+
availableModels: Array.isArray(payload.deepseekModels.availableModels)
|
|
2500
|
+
? payload.deepseekModels.availableModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2501
|
+
: [],
|
|
2502
|
+
enabledModels: Array.isArray(payload.deepseekModels.enabledModels)
|
|
2503
|
+
? payload.deepseekModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2504
|
+
: [],
|
|
2505
|
+
}
|
|
2506
|
+
: null,
|
|
2473
2507
|
};
|
|
2474
2508
|
}
|
|
2475
2509
|
|
|
@@ -2546,6 +2580,10 @@ function renderModelVariantCatalogBody() {
|
|
|
2546
2580
|
return entryProvider === 'grok';
|
|
2547
2581
|
}
|
|
2548
2582
|
if (entryProvider === 'grok') return false;
|
|
2583
|
+
if (activeTab === 'deepseek') {
|
|
2584
|
+
return entryProvider === 'deepseek';
|
|
2585
|
+
}
|
|
2586
|
+
if (entryProvider === 'deepseek') return false;
|
|
2549
2587
|
if (activeTab === 'openai') {
|
|
2550
2588
|
return hasOpenAIByok || entryProvider === 'openai-byok';
|
|
2551
2589
|
}
|
|
@@ -2557,6 +2595,7 @@ function renderModelVariantCatalogBody() {
|
|
|
2557
2595
|
&& provider !== 'claude'
|
|
2558
2596
|
&& provider !== 'cursor'
|
|
2559
2597
|
&& provider !== 'grok'
|
|
2598
|
+
&& provider !== 'deepseek'
|
|
2560
2599
|
&& provider !== CLAUDE_CLOUD_PROVIDER
|
|
2561
2600
|
));
|
|
2562
2601
|
}
|
|
@@ -2667,6 +2706,29 @@ function renderModelVariantCatalogBody() {
|
|
|
2667
2706
|
}
|
|
2668
2707
|
grouped.set('grok', providerRows);
|
|
2669
2708
|
}
|
|
2709
|
+
const deepseekCatalog = modelVariantCatalogState.deepseekModels;
|
|
2710
|
+
if (deepseekCatalog?.availableModels?.length) {
|
|
2711
|
+
const deepseekEnabledSet = new Set(deepseekCatalog.enabledModels || []);
|
|
2712
|
+
const deepseekDefaultModel = String(deepseekCatalog.defaultModel || '').trim();
|
|
2713
|
+
const providerRows = [];
|
|
2714
|
+
for (const [index, deepseekModelId] of deepseekCatalog.availableModels.entries()) {
|
|
2715
|
+
const isDefaultModel = deepseekModelId === deepseekDefaultModel;
|
|
2716
|
+
providerRows.push({
|
|
2717
|
+
variantId: `${deepseekModelId}--provider-deepseek`,
|
|
2718
|
+
baseModelId: deepseekModelId,
|
|
2719
|
+
provider: 'deepseek',
|
|
2720
|
+
label: humanizeModelLabel(deepseekModelId),
|
|
2721
|
+
releaseStatus: null,
|
|
2722
|
+
reasoningEffort: null,
|
|
2723
|
+
// The default model always stays enabled, as for the other providers.
|
|
2724
|
+
selectable: !isDefaultModel,
|
|
2725
|
+
enabled: deepseekEnabledSet.has(deepseekModelId) || isDefaultModel,
|
|
2726
|
+
deepseekModelId,
|
|
2727
|
+
sortOrder: index,
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
grouped.set('deepseek', providerRows);
|
|
2731
|
+
}
|
|
2670
2732
|
const selected = new Set(modelVariantCatalogState.enabledVariantIds);
|
|
2671
2733
|
const selectedOrder = new Map(
|
|
2672
2734
|
modelVariantCatalogState.enabledVariantIds.map((variantId, index) => [variantId, index]),
|
|
@@ -2702,15 +2764,18 @@ function renderModelVariantCatalogBody() {
|
|
|
2702
2764
|
const hasAnthropicTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'anthropic'));
|
|
2703
2765
|
const hasCursorTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'cursor'));
|
|
2704
2766
|
const hasGrokTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'grok'));
|
|
2767
|
+
const hasDeepSeekTab = providerOrder.some((providerKey) => providerBelongsToTab(providerKey, 'deepseek'));
|
|
2705
2768
|
if (modelVariantCatalogProviderTab === 'openai' && !hasOpenAITab) modelVariantCatalogProviderTab = 'copilot';
|
|
2706
2769
|
if (modelVariantCatalogProviderTab === 'anthropic' && !hasAnthropicTab) modelVariantCatalogProviderTab = 'copilot';
|
|
2707
2770
|
if (modelVariantCatalogProviderTab === 'cursor' && !hasCursorTab) modelVariantCatalogProviderTab = 'copilot';
|
|
2708
2771
|
if (modelVariantCatalogProviderTab === 'grok' && !hasGrokTab) modelVariantCatalogProviderTab = 'copilot';
|
|
2772
|
+
if (modelVariantCatalogProviderTab === 'deepseek' && !hasDeepSeekTab) modelVariantCatalogProviderTab = 'copilot';
|
|
2709
2773
|
if (modelVariantCatalogProviderTab === 'copilot' && !hasCopilotTab) {
|
|
2710
2774
|
if (hasOpenAITab) modelVariantCatalogProviderTab = 'openai';
|
|
2711
2775
|
else if (hasAnthropicTab) modelVariantCatalogProviderTab = 'anthropic';
|
|
2712
2776
|
else if (hasCursorTab) modelVariantCatalogProviderTab = 'cursor';
|
|
2713
2777
|
else if (hasGrokTab) modelVariantCatalogProviderTab = 'grok';
|
|
2778
|
+
else if (hasDeepSeekTab) modelVariantCatalogProviderTab = 'deepseek';
|
|
2714
2779
|
}
|
|
2715
2780
|
const visibleProviderOrder = providerOrder.filter((providerKey) => providerBelongsToTab(providerKey, modelVariantCatalogProviderTab));
|
|
2716
2781
|
const providerTabButtons = `
|
|
@@ -2720,6 +2785,7 @@ function renderModelVariantCatalogBody() {
|
|
|
2720
2785
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'anthropic' ? ' active' : ''}" data-model-provider-tab="anthropic" ${hasAnthropicTab ? '' : 'disabled'}>Claude SDK</button>
|
|
2721
2786
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'cursor' ? ' active' : ''}" data-model-provider-tab="cursor" ${hasCursorTab ? '' : 'disabled'}>Cursor SDK</button>
|
|
2722
2787
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'grok' ? ' active' : ''}" data-model-provider-tab="grok" ${hasGrokTab ? '' : 'disabled'}>Grok</button>
|
|
2788
|
+
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'deepseek' ? ' active' : ''}" data-model-provider-tab="deepseek" ${hasDeepSeekTab ? '' : 'disabled'}>DeepSeek</button>
|
|
2723
2789
|
</div>
|
|
2724
2790
|
`;
|
|
2725
2791
|
const warnings = [
|
|
@@ -2772,9 +2838,9 @@ function renderModelVariantCatalogBody() {
|
|
|
2772
2838
|
: '';
|
|
2773
2839
|
const variantRowsHtml = variantRows.map((row) => {
|
|
2774
2840
|
const selectable = row.selectable !== false;
|
|
2775
|
-
// SDK-provider rows (Claude/Cursor/Grok) carry their own enabled
|
|
2776
|
-
// Copilot variant rows read the enabledVariantIds selection.
|
|
2777
|
-
const sdkProviderModelId = row.claudeModelId || row.cursorModelId || row.grokModelId || '';
|
|
2841
|
+
// SDK-provider rows (Claude/Cursor/Grok/DeepSeek) carry their own enabled
|
|
2842
|
+
// flag; only Copilot variant rows read the enabledVariantIds selection.
|
|
2843
|
+
const sdkProviderModelId = row.claudeModelId || row.cursorModelId || row.grokModelId || row.deepseekModelId || '';
|
|
2778
2844
|
const checked = sdkProviderModelId
|
|
2779
2845
|
? row.enabled === true
|
|
2780
2846
|
: (selectable && selected.has(row.variantId));
|
|
@@ -2794,7 +2860,9 @@ function renderModelVariantCatalogBody() {
|
|
|
2794
2860
|
? ` data-claude-model="${escHtml(row.claudeModelId)}"`
|
|
2795
2861
|
: (row.cursorModelId
|
|
2796
2862
|
? ` data-cursor-model="${escHtml(row.cursorModelId)}"`
|
|
2797
|
-
: (row.grokModelId
|
|
2863
|
+
: (row.grokModelId
|
|
2864
|
+
? ` data-grok-model="${escHtml(row.grokModelId)}"`
|
|
2865
|
+
: (row.deepseekModelId ? ` data-deepseek-model="${escHtml(row.deepseekModelId)}"` : '')));
|
|
2798
2866
|
return `
|
|
2799
2867
|
<label class="model-variant-row">
|
|
2800
2868
|
<input class="model-variant-checkbox" type="checkbox" data-selectable="${selectable ? '1' : '0'}" data-variant-id="${escHtml(row.variantId)}"${sdkModelAttr} ${checked ? 'checked' : ''} ${selectable ? '' : 'disabled'}>
|
|
@@ -2920,12 +2988,17 @@ async function saveSelectedModelsFromModal() {
|
|
|
2920
2988
|
.filter((input) => input.checked)
|
|
2921
2989
|
.map((input) => String(input.getAttribute('data-grok-model') || '').trim())
|
|
2922
2990
|
.filter(Boolean);
|
|
2991
|
+
const deepseekCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-deepseek-model]'));
|
|
2992
|
+
const selectedDeepSeekModels = deepseekCheckboxes
|
|
2993
|
+
.filter((input) => input.checked)
|
|
2994
|
+
.map((input) => String(input.getAttribute('data-deepseek-model') || '').trim())
|
|
2995
|
+
.filter(Boolean);
|
|
2923
2996
|
// Only the active provider tab's rows are in the DOM, but the PATCH
|
|
2924
2997
|
// replaces the WHOLE enabled set — so the save must only change rows the
|
|
2925
2998
|
// user could actually see. Off-screen variants keep their stored state,
|
|
2926
2999
|
// otherwise saving from e.g. the OpenAI tab silently disables every
|
|
2927
3000
|
// Copilot-only vendor group (Anthropic, Google, …).
|
|
2928
|
-
const renderedVariantCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-selectable="1"]:not([data-claude-model]):not([data-cursor-model]):not([data-grok-model])'));
|
|
3001
|
+
const renderedVariantCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-selectable="1"]:not([data-claude-model]):not([data-cursor-model]):not([data-grok-model]):not([data-deepseek-model])'));
|
|
2929
3002
|
const renderedVariantIds = new Set(renderedVariantCheckboxes
|
|
2930
3003
|
.map((input) => String(input.getAttribute('data-variant-id') || '').trim())
|
|
2931
3004
|
.filter(Boolean));
|
|
@@ -2966,7 +3039,12 @@ async function saveSelectedModelsFromModal() {
|
|
|
2966
3039
|
if (!savedGrok) throw new Error('Failed to save Grok model selection');
|
|
2967
3040
|
applyGrokSettingsState(savedGrok);
|
|
2968
3041
|
}
|
|
2969
|
-
if (
|
|
3042
|
+
if (deepseekCheckboxes.length) {
|
|
3043
|
+
const savedDeepSeek = await updateDeepSeekSettings({ enabledModels: selectedDeepSeekModels });
|
|
3044
|
+
if (!savedDeepSeek) throw new Error('Failed to save DeepSeek model selection');
|
|
3045
|
+
applyDeepSeekSettingsState(savedDeepSeek);
|
|
3046
|
+
}
|
|
3047
|
+
if (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length || deepseekCheckboxes.length) {
|
|
2970
3048
|
const refreshedCatalog = await loadModelVariantCatalog();
|
|
2971
3049
|
if (refreshedCatalog) applyModelVariantCatalogState(refreshedCatalog);
|
|
2972
3050
|
}
|
|
@@ -3346,6 +3424,41 @@ function initAttributionControl() {
|
|
|
3346
3424
|
});
|
|
3347
3425
|
}
|
|
3348
3426
|
|
|
3427
|
+
// The owner's "agents in this conversation may start sessions" in the 🧠
|
|
3428
|
+
// modal: one request per change; the store follows the answer (other tabs
|
|
3429
|
+
// follow the socket event).
|
|
3430
|
+
let agentSessionsControlUpdateInFlight = false;
|
|
3431
|
+
function initAgentSessionsControl() {
|
|
3432
|
+
const body = document.getElementById('summary-modal-body');
|
|
3433
|
+
if (!body || body.dataset.agentSessionsControlBound === 'true') return;
|
|
3434
|
+
body.dataset.agentSessionsControlBound = 'true';
|
|
3435
|
+
body.addEventListener('change', async (event) => {
|
|
3436
|
+
const box = event.target?.closest?.('#ctx-may-create-sessions');
|
|
3437
|
+
if (!box) return;
|
|
3438
|
+
const convId = String(box.dataset.conversationId || '').trim();
|
|
3439
|
+
if (!convId || agentSessionsControlUpdateInFlight) return;
|
|
3440
|
+
const wanted = box.checked === true;
|
|
3441
|
+
agentSessionsControlUpdateInFlight = true;
|
|
3442
|
+
box.disabled = true;
|
|
3443
|
+
try {
|
|
3444
|
+
const response = await updateConversationPreferences(convId, { clientId: CLIENT_ID, mayCreateSessions: wanted });
|
|
3445
|
+
if (!response) throw new Error('The relay refused the change');
|
|
3446
|
+
const stored = response.mayCreateSessions === true;
|
|
3447
|
+
if (conversations[convId]) conversations[convId] = { ...conversations[convId], mayCreateSessions: stored };
|
|
3448
|
+
box.checked = stored;
|
|
3449
|
+
showTransientRelayNotice(stored
|
|
3450
|
+
? 'Agents in this conversation may now start sessions.'
|
|
3451
|
+
: 'Agents in this conversation can no longer start sessions.');
|
|
3452
|
+
} catch (error) {
|
|
3453
|
+
box.checked = !wanted;
|
|
3454
|
+
showTransientRelayNotice(`Could not change the setting: ${error?.message || error}`);
|
|
3455
|
+
} finally {
|
|
3456
|
+
box.disabled = false;
|
|
3457
|
+
agentSessionsControlUpdateInFlight = false;
|
|
3458
|
+
}
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3349
3462
|
function initThinkingControl() {
|
|
3350
3463
|
const body = document.getElementById('summary-modal-body');
|
|
3351
3464
|
if (!body || body.dataset.thinkingBound === '1') return;
|
|
@@ -3442,7 +3555,9 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3442
3555
|
? 'Claude Cloud'
|
|
3443
3556
|
: (payload.providerType === 'cursor'
|
|
3444
3557
|
? 'Cursor'
|
|
3445
|
-
: (payload.providerType === 'grok'
|
|
3558
|
+
: (payload.providerType === 'grok'
|
|
3559
|
+
? 'Grok'
|
|
3560
|
+
: (payload.providerType === 'deepseek' ? 'DeepSeek' : 'Copilot'))));
|
|
3446
3561
|
const subtitle = sessionId
|
|
3447
3562
|
? `${providerLabel} session ${sessionId.slice(0, 8)}`
|
|
3448
3563
|
: (trimmedConvId ? `Conversation ${trimmedConvId.slice(0, 8)}` : 'No conversation selected');
|
|
@@ -3451,14 +3566,17 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3451
3566
|
// (older relays send neither, and the notes fall back to draft vs stored).
|
|
3452
3567
|
const sessionField = (key) => payload[key] ?? payload.contextUsage?.[key] ?? null;
|
|
3453
3568
|
const modelContextLimit = sessionField('modelContextLimit');
|
|
3569
|
+
// A DeepSeek chat runs the same Claude Code runner, so it has the same
|
|
3570
|
+
// compaction window to show and to steer.
|
|
3571
|
+
const runsClaudeCode = payload.providerType === 'claude' || payload.providerType === 'deepseek';
|
|
3454
3572
|
const usageHtml = renderContextUsageHtml(payload.contextUsage, {
|
|
3455
|
-
compactionWindow:
|
|
3573
|
+
compactionWindow: runsClaudeCode,
|
|
3456
3574
|
modelContextLimit,
|
|
3457
3575
|
});
|
|
3458
3576
|
// The slider's writes need the conversation id even when the modal was
|
|
3459
3577
|
// opened through an sdk_session_id.
|
|
3460
3578
|
const boundConversationId = String(
|
|
3461
|
-
payload.resolvedConversationId || (
|
|
3579
|
+
payload.resolvedConversationId || (runsClaudeCode ? trimmedConvId : '') || '',
|
|
3462
3580
|
).trim();
|
|
3463
3581
|
// A draft for another conversation is saved before this one takes over.
|
|
3464
3582
|
if (autoCompactDraft.conversationId && autoCompactDraft.conversationId !== boundConversationId) {
|
|
@@ -3474,9 +3592,9 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3474
3592
|
usedTokens: payload.contextUsage?.totalTokens ?? null,
|
|
3475
3593
|
};
|
|
3476
3594
|
const draftWindow = autoCompactDraft.conversationId === boundConversationId ? autoCompactDraft.window : undefined;
|
|
3477
|
-
// Claude
|
|
3478
|
-
// session-settings surface) to steer.
|
|
3479
|
-
const autoCompactHtml =
|
|
3595
|
+
// Claude Code only (Claude and DeepSeek chats): no other provider exposes a
|
|
3596
|
+
// compaction window (or any session-settings surface) to steer.
|
|
3597
|
+
const autoCompactHtml = runsClaudeCode
|
|
3480
3598
|
? renderAutoCompactControlHtml({
|
|
3481
3599
|
autoCompactWindow: payload.autoCompactWindow ?? null,
|
|
3482
3600
|
autoCompactThreshold: payload.contextUsage?.autoCompactThreshold ?? null,
|
|
@@ -3511,14 +3629,21 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3511
3629
|
enabled: payload.thinkingEnabled !== false,
|
|
3512
3630
|
display: payload.thinkingDisplay || 'summarized',
|
|
3513
3631
|
};
|
|
3632
|
+
// For a session an agent started (any provider): may its agents start
|
|
3633
|
+
// sessions? Only meaningful there, so only shown there.
|
|
3634
|
+
const settingsConversationId = String(payload.resolvedConversationId || trimmedConvId || '').trim();
|
|
3635
|
+
const settingsConversation = settingsConversationId ? conversations[settingsConversationId] : null;
|
|
3636
|
+
const agentSessionsHtml = settingsConversation?.origin
|
|
3637
|
+
? renderAgentSessionsControlHtml({ conversationId: settingsConversationId, mayCreateSessions: settingsConversation.mayCreateSessions === true })
|
|
3638
|
+
: '';
|
|
3514
3639
|
const detailText = String(payload.text || '').trim();
|
|
3515
3640
|
// The structured breakdown is the answer; the runtime's own text dump stays
|
|
3516
3641
|
// available underneath for the details it carries that categories don't.
|
|
3517
3642
|
const bodyHtml = usageHtml
|
|
3518
|
-
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${attributionHtml}${detailText
|
|
3643
|
+
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
3519
3644
|
? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
|
|
3520
3645
|
: ''}`
|
|
3521
|
-
: `${autoCompactHtml}${thinkingHtml}${attributionHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3646
|
+
: `${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3522
3647
|
|
|
3523
3648
|
renderSummaryModalContent({
|
|
3524
3649
|
title: 'Context usage',
|
|
@@ -4387,6 +4512,8 @@ function syncChatTitleControls() {
|
|
|
4387
4512
|
syncConversationOriginHeader();
|
|
4388
4513
|
// Relay events: "watching N sessions" on a watcher, "watched" on a watched one.
|
|
4389
4514
|
syncRelayEventHeader();
|
|
4515
|
+
// Relay schedules: "N scheduled" with its list and Cancel buttons.
|
|
4516
|
+
syncRelayScheduleHeader();
|
|
4390
4517
|
syncChatTitleWatcherIndicator();
|
|
4391
4518
|
}
|
|
4392
4519
|
|
|
@@ -4524,6 +4651,7 @@ initSocketHandlers({
|
|
|
4524
4651
|
applyCopilotSettingsState,
|
|
4525
4652
|
applyGrokSettingsState,
|
|
4526
4653
|
applyCursorSettingsState,
|
|
4654
|
+
applyDeepSeekSettingsState,
|
|
4527
4655
|
});
|
|
4528
4656
|
|
|
4529
4657
|
initCwdPicker({
|
|
@@ -4884,6 +5012,7 @@ async function initApp() {
|
|
|
4884
5012
|
initAutoCompactWindowControl();
|
|
4885
5013
|
initThinkingControl();
|
|
4886
5014
|
initAttributionControl();
|
|
5015
|
+
initAgentSessionsControl();
|
|
4887
5016
|
initModeSelector();
|
|
4888
5017
|
initModelSelector();
|
|
4889
5018
|
initReasoningSelector();
|
|
@@ -4905,11 +5034,14 @@ async function initApp() {
|
|
|
4905
5034
|
// Loaded here for provider readiness: whether any provider can run a chat
|
|
4906
5035
|
// decides what the empty screen, New Chat and the composer offer.
|
|
4907
5036
|
const claudeSettings = await refreshClaudeSettingsState();
|
|
5037
|
+
// null on a relay without DeepSeek: not offered, as for Claude Cloud.
|
|
5038
|
+
const deepseekSettings = await refreshDeepSeekSettingsState();
|
|
4908
5039
|
updateProviderReadinessInputs({
|
|
4909
5040
|
openai: openAISettings || null,
|
|
4910
5041
|
cursor: cursorSettings || null,
|
|
4911
5042
|
grok: grokSettings || null,
|
|
4912
5043
|
claude: claudeSettings || null,
|
|
5044
|
+
deepseek: deepseekSettings || null,
|
|
4913
5045
|
});
|
|
4914
5046
|
// Not awaited: only a Claude Cloud conversation needs it (its model list),
|
|
4915
5047
|
// and the subscription below rebuilds that list when the answer arrives. A
|
|
@@ -4965,6 +5097,11 @@ async function initApp() {
|
|
|
4965
5097
|
// The sidebar's "watched" badges follow the subscriptions.
|
|
4966
5098
|
onRelayEventSubscriptionsChanged(() => renderConvList());
|
|
4967
5099
|
void loadRelayEventSubscriptions();
|
|
5100
|
+
// The sidebar's "scheduled" markers follow the schedules.
|
|
5101
|
+
onRelaySchedulesChanged(() => renderConvList());
|
|
5102
|
+
void loadRelaySchedules();
|
|
5103
|
+
// The Inbox's count, and the cards of questions that don't block.
|
|
5104
|
+
startInbox();
|
|
4968
5105
|
const pushConversationId = consumePushConversationDeepLink();
|
|
4969
5106
|
if (pushConversationId) {
|
|
4970
5107
|
await openConversation(pushConversationId).catch(() => {});
|
|
@@ -5080,6 +5217,10 @@ window.cancelRunningUpdate = cancelRunningUpdate;
|
|
|
5080
5217
|
window.dismissAvailableUpdate = dismissAvailableUpdate;
|
|
5081
5218
|
window.dismissUpdateOutcome = dismissUpdateOutcome;
|
|
5082
5219
|
window.saveGrokSettings = saveGrokSettings;
|
|
5220
|
+
window.saveDeepSeekSettings = saveDeepSeekSettings;
|
|
5221
|
+
window.removeDeepSeekSettings = removeDeepSeekSettings;
|
|
5222
|
+
window.toggleDeepSeekProvider = toggleDeepSeekProvider;
|
|
5223
|
+
window.importDeepSeekUsageExport = importDeepSeekUsageExport;
|
|
5083
5224
|
window.toggleGrokProvider = toggleGrokProvider;
|
|
5084
5225
|
window.saveClaudeCloudSettings = saveClaudeCloudSettings;
|
|
5085
5226
|
window.toggleClaudeCloudProvider = toggleClaudeCloudProvider;
|
|
@@ -5165,7 +5306,12 @@ window.toggleArchivedConversations = toggleArchivedConversations;
|
|
|
5165
5306
|
window.refreshNewConversationCloudRepoList = refreshNewConversationCloudRepoList;
|
|
5166
5307
|
window.openConversation = openConversation;
|
|
5167
5308
|
window.openOriginConversation = openOriginConversation;
|
|
5168
|
-
|
|
5309
|
+
// The Inbox and Status share the transcript's place: opening one leaves the other.
|
|
5310
|
+
window.toggleStatusView = () => {
|
|
5311
|
+
leaveInboxView();
|
|
5312
|
+
return toggleStatusView();
|
|
5313
|
+
};
|
|
5314
|
+
window.toggleInboxView = toggleInboxView;
|
|
5169
5315
|
window.refreshConversations = refreshConversations;
|
|
5170
5316
|
window.renderConvList = renderConvList;
|
|
5171
5317
|
window.handleAttachmentInput = handleAttachmentInput;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Mirrors shared/model-id.mjs, which the browser cannot import (only
|
|
2
2
|
// server/public is served).
|
|
3
3
|
const CLAUDE_LONG_CONTEXT_LIMIT_TOKENS = 1000000;
|
|
4
|
+
// Mirrors shared/deepseek-models.mjs for the same reason.
|
|
5
|
+
const DEEPSEEK_CONTEXT_WINDOW_TOKENS = 1048576;
|
|
4
6
|
export const UNKNOWN_WINDOW_LABEL = '—';
|
|
5
7
|
|
|
6
8
|
// Shared by the context chip and both model pickers so a window reads the same
|
|
@@ -59,6 +61,8 @@ export function buildContextTierOptions({
|
|
|
59
61
|
// metadata either (that number is Copilot's): one blank default, as for a
|
|
60
62
|
// Claude model whose window is unknown.
|
|
61
63
|
if (provider === 'claude-cloud') return [{ value: 'default', label: UNKNOWN_WINDOW_LABEL }];
|
|
64
|
+
// DeepSeek's models have one window, 1M: no tier to choose.
|
|
65
|
+
if (provider === 'deepseek') return [{ value: 'default', label: tokenLabel(DEEPSEEK_CONTEXT_WINDOW_TOKENS) }];
|
|
62
66
|
const claudeTierList = provider === 'claude' ? claudeTiersForModel(modelId, claudeTiers) : null;
|
|
63
67
|
if (claudeTierList) {
|
|
64
68
|
return claudeTierList
|
|
@@ -350,6 +350,23 @@ ${renderThinkingButtons('display', THINKING_DISPLAY_CHOICES, displayKey)}
|
|
|
350
350
|
`;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The owner's switch for a session an agent started: may its agents start
|
|
355
|
+
* sessions themselves (the remote_relay tool's one-level rule lifted for it)?
|
|
356
|
+
* Only the owner sets it; no agent can.
|
|
357
|
+
*/
|
|
358
|
+
export function renderAgentSessionsControlHtml({ conversationId = '', mayCreateSessions = false } = {}) {
|
|
359
|
+
return `
|
|
360
|
+
<div class="ctx-agent-sessions">
|
|
361
|
+
<label class="ctx-agent-sessions-row">
|
|
362
|
+
<input type="checkbox" id="ctx-may-create-sessions" data-conversation-id="${escapeHtml(conversationId)}"${mayCreateSessions === true ? ' checked' : ''}>
|
|
363
|
+
<span>Agents in this conversation may start sessions (even though an agent started it)</span>
|
|
364
|
+
</label>
|
|
365
|
+
<div class="ctx-thinking-note">An agent started this session, so its agents cannot start sessions of their own unless you allow it here. The limit of sessions at work, your Allow for the first one and the relay's "Agents may start and use sessions" setting still apply.</div>
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
|
|
353
370
|
// A model limit is a round figure: 1M rather than 1.0M.
|
|
354
371
|
function formatModelLimit(value) {
|
|
355
372
|
const n = toNullableNumber(value);
|
|
@@ -210,12 +210,47 @@ export function pageConversationRows(rows, groups = null, { limit = 40, beforeCo
|
|
|
210
210
|
* `isOpen(groupId, group)` decides the fold; `matches(row)` keeps rows that
|
|
211
211
|
* match a filter (a group shows when any row matches: its head as context
|
|
212
212
|
* with the matching members, or whole when the head or its name matches).
|
|
213
|
+
*
|
|
214
|
+
* With `emptyGroups`, an owner's group an agent emptied (`keptEmptyAt`, no
|
|
215
|
+
* row here) shows too: a head item without members (`group.empty`), placed
|
|
216
|
+
* by the time it was emptied; under a filter only when its name matches.
|
|
213
217
|
*/
|
|
214
218
|
export function arrangeConversationList(rows, groups = null, {
|
|
215
219
|
isOpen = () => true,
|
|
216
220
|
matches = null,
|
|
217
221
|
groupNameMatches = null,
|
|
222
|
+
emptyGroups = false,
|
|
218
223
|
} = {}) {
|
|
224
|
+
const items = arrangeGroupedRows(rows, groups, { isOpen, matches, groupNameMatches });
|
|
225
|
+
if (!emptyGroups || !groups) return items;
|
|
226
|
+
const present = new Set((Array.isArray(rows) ? rows : []).map(rowGroupId).filter(Boolean));
|
|
227
|
+
const entries = typeof groups.entries === 'function' && typeof groups.get === 'function' ? [...groups.entries()] : Object.entries(groups);
|
|
228
|
+
const kept = entries
|
|
229
|
+
.map(([id, info]) => ({ id: toText(id), info }))
|
|
230
|
+
.filter(({ id, info }) => id && toText(info?.keptEmptyAt) && !present.has(id))
|
|
231
|
+
.filter(({ info }) => !matches || (typeof groupNameMatches === 'function' && groupNameMatches(info, null)))
|
|
232
|
+
.map(({ id, info }) => ({
|
|
233
|
+
kind: 'head',
|
|
234
|
+
conversation: null,
|
|
235
|
+
group: { id, info, head: null, members: [], activityMs: parseConversationTimeMs(info.keptEmptyAt), open: false, contextOnly: false, empty: true },
|
|
236
|
+
}))
|
|
237
|
+
.sort((a, b) => (b.group.activityMs - a.group.activityMs) || b.group.id.localeCompare(a.group.id));
|
|
238
|
+
if (!kept.length) return items;
|
|
239
|
+
// Merged in by time, before the first row or group that is older.
|
|
240
|
+
const timeOf = (item) => (item.kind === 'head' ? item.group.activityMs : rowOwnTimeMs(item.conversation));
|
|
241
|
+
const out = [];
|
|
242
|
+
let next = 0;
|
|
243
|
+
for (const item of items) {
|
|
244
|
+
if (item.kind !== 'member') {
|
|
245
|
+
while (next < kept.length && kept[next].group.activityMs > timeOf(item)) out.push(kept[next++]);
|
|
246
|
+
}
|
|
247
|
+
out.push(item);
|
|
248
|
+
}
|
|
249
|
+
while (next < kept.length) out.push(kept[next++]);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function arrangeGroupedRows(rows, groups, { isOpen, matches, groupNameMatches }) {
|
|
219
254
|
const ordered = orderConversationRows(rows, groups);
|
|
220
255
|
const activity = groupActivityTimes(ordered);
|
|
221
256
|
const items = [];
|
|
@@ -23,6 +23,10 @@ export function isConversationUsingGrokProvider(conversation = null) {
|
|
|
23
23
|
return resolveConversationProviderType(conversation) === 'grok';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function isConversationUsingDeepSeekProvider(conversation = null) {
|
|
27
|
+
return resolveConversationProviderType(conversation) === 'deepseek';
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
// Its own provider, never a flavour of `claude`: the session runs in a sandbox
|
|
27
31
|
// at Anthropic, so nothing that is true of a local Claude worker (modes,
|
|
28
32
|
// effort, steering, a working directory) may be assumed for it.
|
|
@@ -39,6 +43,7 @@ const PROVIDER_NAMES = Object.freeze({
|
|
|
39
43
|
'claude-cloud': 'Claude Cloud',
|
|
40
44
|
cursor: 'Cursor',
|
|
41
45
|
grok: 'Grok',
|
|
46
|
+
deepseek: 'DeepSeek',
|
|
42
47
|
github: 'Copilot',
|
|
43
48
|
'github-copilot': 'Copilot',
|
|
44
49
|
});
|
|
@@ -76,6 +81,7 @@ const SESSION_LOCK_LABELS = {
|
|
|
76
81
|
claude: 'Claude SDK',
|
|
77
82
|
cursor: 'Cursor SDK',
|
|
78
83
|
grok: 'Grok',
|
|
84
|
+
deepseek: 'DeepSeek',
|
|
79
85
|
'claude-cloud': `${CLAUDE_CLOUD_MARKER} Claude Cloud`,
|
|
80
86
|
};
|
|
81
87
|
|
|
@@ -96,6 +102,7 @@ export function sessionLockProviderKey({ providerType = '', model = '' } = {}) {
|
|
|
96
102
|
if (normalized === 'claude-cloud') return 'claude-cloud';
|
|
97
103
|
if (normalized === 'cursor') return 'cursor';
|
|
98
104
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
105
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
99
106
|
if (normalized === 'github' || normalized === 'github-copilot') return 'github';
|
|
100
107
|
return '';
|
|
101
108
|
}
|
|
@@ -3523,6 +3523,8 @@ function runRelayErrorCta(cta) {
|
|
|
3523
3523
|
}
|
|
3524
3524
|
if (cta === 'open-grok-settings') openSettingsModal('providers', 'grok');
|
|
3525
3525
|
if (cta === 'open-claude-settings') openSettingsModal('providers', 'claude');
|
|
3526
|
+
if (cta === 'open-deepseek-settings') openSettingsModal('providers', 'deepseek');
|
|
3527
|
+
if (cta === 'open-deepseek-top-up') window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener');
|
|
3526
3528
|
}
|
|
3527
3529
|
|
|
3528
3530
|
function handleBubbleActionClick(event) {
|