@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.
Files changed (109) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/CHANGELOG.md +305 -0
  3. package/README.md +12 -11
  4. package/package.json +2 -1
  5. package/server/claude-worker/claude-attachments.mjs +7 -4
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
  7. package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
  8. package/server/claude-worker/claude-session-process.mjs +131 -33
  9. package/server/claude-worker/claude-session-worker-main.mjs +266 -0
  10. package/server/claude-worker/claude-session-worker.mjs +2 -253
  11. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  12. package/server/claude-worker/claude-turn-publisher.mjs +282 -13
  13. package/server/claude-worker/claude-usage-totals.mjs +108 -0
  14. package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
  15. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  16. package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
  17. package/server/db-schema.mjs +115 -0
  18. package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
  19. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  20. package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
  21. package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
  22. package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
  23. package/server/migrations/0022-relay-schedules.mjs +142 -0
  24. package/server/migrations/0023-relay-asks.mjs +141 -0
  25. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  26. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  27. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  28. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  29. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  30. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  31. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  32. package/server/public/app/api-client.js +53 -0
  33. package/server/public/app/ask-user-view.js +3 -0
  34. package/server/public/app/bootstrap.js +162 -16
  35. package/server/public/app/context-tier-options.mjs +4 -0
  36. package/server/public/app/context-usage-view.mjs +17 -0
  37. package/server/public/app/conversation-group-order.mjs +35 -0
  38. package/server/public/app/conversation-provider-indicator.mjs +7 -0
  39. package/server/public/app/conversation-view.js +2 -0
  40. package/server/public/app/deepseek-settings-ui.js +342 -0
  41. package/server/public/app/inbox-ui.js +367 -0
  42. package/server/public/app/inbox-view.mjs +370 -0
  43. package/server/public/app/journal-view.js +54 -5
  44. package/server/public/app/model-selector-options.mjs +11 -0
  45. package/server/public/app/model-vision-support.mjs +2 -0
  46. package/server/public/app/no-conversation-state.mjs +1 -1
  47. package/server/public/app/plan-usage-view.mjs +165 -13
  48. package/server/public/app/provider-readiness.mjs +4 -1
  49. package/server/public/app/relay-error-ctas.mjs +8 -0
  50. package/server/public/app/relay-event-subscriptions-ui.js +4 -2
  51. package/server/public/app/relay-event-view.mjs +33 -7
  52. package/server/public/app/relay-schedule-view.mjs +119 -0
  53. package/server/public/app/relay-schedules-ui.js +172 -0
  54. package/server/public/app/settings-modal.js +11 -0
  55. package/server/public/app/settings-tabs.js +1 -1
  56. package/server/public/app/socket-handlers.js +37 -3
  57. package/server/public/app/store.js +18 -5
  58. package/server/public/index.html +567 -35
  59. package/server/relay-tools.md +18 -3
  60. package/server/repositories/conversation-group-repository.mjs +110 -17
  61. package/server/repositories/remote-relay-repository.mjs +20 -2
  62. package/server/repositories/session-repository.mjs +7 -1
  63. package/server/routes/ask-user-routes.mjs +57 -3
  64. package/server/routes/cache-routes.mjs +6 -0
  65. package/server/routes/conversation-group-routes.mjs +19 -3
  66. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  67. package/server/routes/messages-routes.mjs +195 -18
  68. package/server/routes/relay-ask-routes.mjs +387 -0
  69. package/server/routes/relay-event-routes.mjs +3 -1
  70. package/server/routes/relay-schedule-routes.mjs +57 -0
  71. package/server/routes/sessions-routes.mjs +932 -97
  72. package/server/server-runtime.mjs +203 -11
  73. package/server/services/about-service.mjs +1 -0
  74. package/server/services/cache-rebuild-service.mjs +19 -2
  75. package/server/services/claude-session-leftovers.mjs +226 -0
  76. package/server/services/conversation-preferences-service.mjs +17 -0
  77. package/server/services/deepseek-balance-service.mjs +102 -0
  78. package/server/services/deepseek-settings-service.mjs +380 -0
  79. package/server/services/deepseek-usage-import.mjs +471 -0
  80. package/server/services/model-variant-catalog-service.mjs +1 -0
  81. package/server/services/plan-usage-contract.mjs +100 -5
  82. package/server/services/plan-usage-deepseek.mjs +931 -0
  83. package/server/services/plan-usage-service.mjs +431 -0
  84. package/server/services/provider-model-selection.mjs +1 -0
  85. package/server/services/provider-reasoning-effort.mjs +15 -2
  86. package/server/services/relay-ask-link-service.mjs +174 -0
  87. package/server/services/relay-ask-service.mjs +604 -0
  88. package/server/services/relay-event-service.mjs +589 -73
  89. package/server/services/relay-schedule-service.mjs +596 -0
  90. package/server/services/remote-relay-caller-context.mjs +89 -20
  91. package/server/services/remote-relay-dispatcher.mjs +958 -31
  92. package/server/services/session-worker-launch-service.mjs +233 -82
  93. package/server/services/worker-secret-redaction.mjs +318 -0
  94. package/shared/context-window-fallbacks.mjs +3 -0
  95. package/shared/deepseek-api.mjs +137 -0
  96. package/shared/deepseek-models.mjs +114 -0
  97. package/shared/deepseek-pricing.mjs +146 -0
  98. package/shared/host-claude-session-env.mjs +101 -0
  99. package/shared/model-id.mjs +7 -0
  100. package/shared/preview-tool-core.mjs +1 -1
  101. package/shared/provider-routing.mjs +2 -2
  102. package/shared/relay-asks.mjs +145 -0
  103. package/shared/relay-events.mjs +268 -18
  104. package/shared/relay-schedules.mjs +128 -0
  105. package/shared/remote-relay-contract.mjs +554 -26
  106. package/shared/remote-relay-tool-core.mjs +16 -5
  107. package/shared/steer-settle-failure.mjs +1 -0
  108. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  109. 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
- return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasClaudeCloud) && !hasNonExclusiveProvider);
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 flag; only
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 ? ` data-grok-model="${escHtml(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 (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length) {
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' ? 'Grok' : 'Copilot')));
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: payload.providerType === 'claude',
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 || (payload.providerType === 'claude' ? trimmedConvId : '') || '',
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-only: no other provider exposes a compaction window (or any
3478
- // session-settings surface) to steer.
3479
- const autoCompactHtml = payload.providerType === 'claude'
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
- window.toggleStatusView = toggleStatusView;
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) {