@ran-sh/dsh-crew 1.1.1 → 1.2.0-rc.5

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 (84) hide show
  1. package/.claude-plugin/marketplace.json +16 -17
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/README.md +44 -15
  4. package/README.zh.md +37 -12
  5. package/agents/ds-flash.md +16 -18
  6. package/agents/ds-pro.md +16 -24
  7. package/agents/ds-reviewer.md +15 -15
  8. package/agents/ds-worker.md +16 -16
  9. package/bin/dsh-crew.mjs +16 -12
  10. package/codex/AGENTS.md +37 -85
  11. package/codex/agents/ds-flash.toml +18 -14
  12. package/codex/agents/ds-pro.toml +18 -15
  13. package/codex/agents/ds-reviewer.toml +17 -13
  14. package/codex/agents/ds-worker.toml +17 -13
  15. package/codex/prompts/dsh-config.md +17 -1
  16. package/codex/prompts/dsh-status.md +5 -1
  17. package/commands/config.md +16 -4
  18. package/commands/status.md +6 -2
  19. package/docs/installation.md +54 -9
  20. package/docs/readiness-matrix.md +14 -6
  21. package/docs/ui-surfaces.md +32 -17
  22. package/lib/client.js +769 -177
  23. package/official-web-bridge/lib/client.js +297 -209
  24. package/official-web-bridge/overlay-entry.mjs +59 -0
  25. package/package.json +6 -3
  26. package/scripts/build-client.mjs +2 -0
  27. package/scripts/setup.mjs +8 -7
  28. package/scripts/verify-crew-ui-polish.mjs +145 -0
  29. package/scripts/verify-history-ui.mjs +97 -0
  30. package/scripts/verify-npm-install.mjs +1 -1
  31. package/src/client/history-panel.tsx +108 -0
  32. package/src/client/host-readiness.mjs +15 -6
  33. package/src/client/index.tsx +91 -69
  34. package/src/client/model-callability-view.mjs +17 -0
  35. package/src/client/panel-chrome.tsx +40 -0
  36. package/src/client/quick-panel.tsx +88 -73
  37. package/src/client/readiness-envelope.mjs +34 -0
  38. package/src/client/task-telemetry.mjs +11 -4
  39. package/src/config-readiness.mjs +181 -115
  40. package/src/extension-contract.mjs +44 -26
  41. package/src/history/admission-gate.mjs +51 -0
  42. package/src/history/archive-store.mjs +261 -0
  43. package/src/history/cleanup-plan.mjs +64 -0
  44. package/src/history/http.mjs +32 -0
  45. package/src/history/operation.mjs +86 -0
  46. package/src/history/runner.mjs +51 -0
  47. package/src/history/runtime.mjs +28 -0
  48. package/src/history/service.mjs +137 -0
  49. package/src/history/state.mjs +27 -0
  50. package/src/hub/index.mjs +141 -24
  51. package/src/hub-client.mjs +17 -15
  52. package/src/image-output.mjs +71 -0
  53. package/src/install/install-legacy.mjs +126 -23
  54. package/src/install/npx-lifecycle.mjs +373 -64
  55. package/src/install/official-frontend-assets.mjs +78 -0
  56. package/src/install/payload-content.mjs +88 -0
  57. package/src/install/windows-startup.mjs +107 -19
  58. package/src/install/windows-supervisor-adapter.mjs +443 -0
  59. package/src/install/windows-supervisor-lifecycle.mjs +782 -0
  60. package/src/install/zcode.mjs +53 -14
  61. package/src/job-contracts.mjs +37 -31
  62. package/src/jobs.mjs +2 -2
  63. package/src/mcp-runtime.mjs +74 -9
  64. package/src/model-callability-contract.mjs +79 -0
  65. package/src/multimodal.mjs +0 -0
  66. package/src/official-web-bridge.mjs +0 -2
  67. package/src/provider-health.mjs +130 -124
  68. package/src/readiness-matrix.mjs +20 -15
  69. package/src/runtime-identity-contract.mjs +34 -0
  70. package/src/runtime-identity.mjs +14 -4
  71. package/src/runtime-readiness-snapshot.mjs +285 -87
  72. package/src/server.mjs +86 -76
  73. package/src/standalone-sdk.mjs +23 -0
  74. package/src/supervisor/restart-request.mjs +64 -10
  75. package/src/workflow-runtime.mjs +28 -13
  76. package/windows/start-dsh-crew.cmd +57 -57
  77. package/windows/start-dsh-crew.ps1 +327 -51
  78. package/windows/supervisor-control.ps1 +467 -0
  79. package/zcode/AGENTS.md +4 -1
  80. package/zcode/agents/ds-reviewer.md +31 -36
  81. package/zcode/agents/ds-worker.md +31 -39
  82. package/zcode/commands/dsh-config.md +17 -6
  83. package/zcode/commands/dsh-status.md +5 -4
  84. package/.mcp.json +0 -8
@@ -2,7 +2,7 @@
2
2
  // global worker configuration, and the live jobs table.
3
3
  // Talks only to the plugin's own loopback routes.
4
4
 
5
- import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
5
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import {
8
8
  openSections,
@@ -12,13 +12,16 @@ import {
12
12
  } from './collapsible-sections.mjs';
13
13
  import { ActivationSummary } from './activation-summary';
14
14
  import { projectHostReadiness, READINESS_STATES } from './host-readiness.mjs';
15
+ import { modelCallabilityState } from './model-callability-view.mjs';
16
+ import { acceptReadinessResponse, readinessExpiryDelay } from './readiness-envelope.mjs';
15
17
  import { CREW_UI_SURFACES, classifyCrewSurface, surfaceResponsibilities } from './surface-detection.mjs';
16
18
  import { aggregateModelInvocations } from './task-telemetry.mjs';
19
+ import { PanelHeader, PanelStyles } from './panel-chrome';
20
+ import { HistoryPanel } from './history-panel';
17
21
 
18
22
  export const inject = ['slots', 'locale'];
19
23
 
20
24
  const API = '/_dsh/dsh-crew';
21
- const CREW_HARNESS_URL = 'http://127.0.0.1:3210/';
22
25
  const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3080/';
23
26
 
24
27
  type AdaptiveConfig = { enabled: boolean; window_size: number; min_samples: number };
@@ -43,9 +46,11 @@ const COPY = {
43
46
  zh: {
44
47
  label: 'DSH Crew',
45
48
  title: 'DSH Crew',
46
- intro: '日常 Crew 控制台:在一个入口里配置编排、角色、模型路由与宿主集成,并跟踪真实 Worker / Reviewer 任务。',
49
+ openOfficial: '打开 3080 官方界面 →',
50
+ intro: '管理工作流、模型路由与宿主集成。展开分区查看和修改设置。',
51
+ surfaceLabel: '3210 / 高级配置',
47
52
  integrations: '集成',
48
- installed: '已安装', notInstalled: '未安装', ready: '可调用', notReady: '未就绪', hud: 'HUD 段',
53
+ installed: '已安装', notInstalled: '未安装', ready: '集成就绪', integrationReady: '集成就绪', modelCallable: '模型可调用', modelNotCallable: '模型未验证', modelUnknown: '模型未知', notReady: '未就绪', hud: 'HUD 段',
49
54
  install: '安装', update: '更新', restore: '还原',
50
55
  confirmRestore: (name: string) => `确定从 ${name} 移除 dsh-crew 集成?(settings 会先备份)`,
51
56
  globalConfig: '全局配置',
@@ -53,11 +58,10 @@ const COPY = {
53
58
  modelCount: (count: number) => `${count} 个模型`, providerCount: (count: number) => `${count} 个 Provider`,
54
59
  jobCount: (count: number) => `${count} 个任务`, runningCount: (count: number) => `${count} 个运行中`,
55
60
  sectionNames: { integrations: 'Codex / Claude / ZCode 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', harnessProviders: 'Harness Providers', providers: '多模态适配器', jobs: '任务状态' },
56
- openHarness: '打开 3210 Crew Harness →',
57
- harnessHint: '底层 Provider、Harness Models 与运行时配置',
61
+ harnessHint: 'DeepSeek 官方 Harness 的 dsh-crew profile:Provider、Harness Models 与运行时配置',
58
62
  hostReadiness: '宿主集成就绪度', hostReadinessHint: '只使用结构化安装与运行时证据;缺少证据不会显示 READY。',
59
- readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', zcode_mcp: 'ZCode MCP', crew_harness: 'Crew Harness', official_bridge: 'Official bridge' },
60
- readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN' },
63
+ readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', zcode_mcp: 'ZCode MCP', crew_harness: 'Crew plugin profile', official_bridge: 'Official bridge' },
64
+ readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN', NOT_APPLICABLE: 'N/A' },
61
65
  globalHint: '修改即时保存到 ~/.config/dsh-crew/config.json;CC / Codex 的新会话自动读取为默认值(会话内可用 /dsh-crew:config 临时覆盖)。',
62
66
  orchestration: 'Agent 编排',
63
67
  enableSubagents: '启用子 Agent',
@@ -103,7 +107,7 @@ const COPY = {
103
107
  presetFlash: 'flash 模式', presetPro: 'pro 模式',
104
108
  multimodal: '多模态',
105
109
  visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
106
- harnessProviders: 'Harness Providers', harnessProviderHint: '3210 Crew Harness 的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。',
110
+ harnessProviders: 'Harness Providers', harnessProviderHint: 'DeepSeek 官方 Harness 的 3210 dsh-crew profile 中的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。',
107
111
  providerState: (state: string) => `状态:${state}`, providerModels: (count: number) => `${count} 个模型`, providerJobs: (count: number) => `${count} 个运行任务`,
108
112
  providerOfficialBuiltin: '· 官方内置(不可删除)', providerSourceUnresolved: '· 来源未解析(安全锁定)',
109
113
  providerLayerMigration: '检测到旧版 profile Provider', providerLayerMigrationHint: '这些 Provider 仍在 Crew profile/base 层,所以 3210 原生页面不会显示删除。请先按迁移计划提升到 Harness 用户设置层;不会复制密钥。', providerLayerMigrationPlan: '迁移计划', providerLayerMigrate: '迁移到用户层', providerLayerMigrating: '迁移中…', providerLayerMigrationConfirm: '确认把这个 Provider 移到 Harness 用户设置层?会先保存可回滚快照,不会复制密钥值。', providerLayerRollback: '回滚迁移',
@@ -159,8 +163,8 @@ const COPY = {
159
163
  save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
160
164
  adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
161
165
  adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
162
- modelActivity: '模型调用概览', modelActivityHint: '聚合本 Hub 内存中最近 500 个任务的真实调用证据,最多显示 50 个模型;不保存提示词、结果或凭据。', noModelActivity: '还没有真实模型调用。',
163
- calls: '调用次数', invocationSource: '任务来源', routingSource: '路由来源', lastCalled: '最近调用', role: '角色', model: '模型', never: '—',
166
+ modelActivity: '模型活动概览', modelActivityHint: '聚合本 Hub 内存中最近 500 个含真实调用证据的任务;每个模型按任务计数(不是推测底层轮次),最多显示 50 个模型。不保存提示词、结果或凭据。', noModelActivity: '还没有真实模型活动。',
167
+ calls: '任务数', invocationSource: '任务来源', routingSource: '路由来源', lastCalled: '最近调用', role: '角色', model: '模型', never: '—',
164
168
  col: { id: '任务', role: '角色', source: '来源', model: '模型', tier: '档位', status: '状态', progress: '进度', tokens: 'tokens ⇅', task: '内容' },
165
169
  working: '处理中…',
166
170
  tips: {
@@ -184,9 +188,11 @@ const COPY = {
184
188
  en: {
185
189
  label: 'DSH Crew',
186
190
  title: 'DSH Crew',
187
- intro: 'The daily Crew console: configure orchestration, roles, model routing, and host integrations in one place while tracking real Worker / Reviewer jobs.',
191
+ intro: 'Manage workflows, model routing and host integrations. Expand a section to edit its settings.',
192
+ surfaceLabel: '3210 / ADVANCED SETTINGS',
193
+ openOfficial: 'Open official frontend (3080) →',
188
194
  integrations: 'Integrations',
189
- installed: 'installed', notInstalled: 'not installed', ready: 'ready', notReady: 'not ready', hud: 'HUD segment',
195
+ installed: 'installed', notInstalled: 'not installed', ready: 'integration ready', integrationReady: 'integration ready', modelCallable: 'model callable', modelNotCallable: 'model not verified', modelUnknown: 'model unknown', notReady: 'not ready', hud: 'HUD segment',
190
196
  install: 'Install', update: 'Update', restore: 'Restore',
191
197
  confirmRestore: (name: string) => `Remove the dsh-crew integration from ${name}? (settings are backed up first)`,
192
198
  globalConfig: 'Global configuration',
@@ -194,11 +200,10 @@ const COPY = {
194
200
  modelCount: (count: number) => `${count} models`, providerCount: (count: number) => `${count} providers`,
195
201
  jobCount: (count: number) => `${count} jobs`, runningCount: (count: number) => `${count} running`,
196
202
  sectionNames: { integrations: 'Codex / Claude / ZCode integration status', workflow: 'Crew workflow settings', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: 'Model priority & dispatch', adaptive: 'Adaptive routing', runtime: 'Runtime / activation boundaries', multimodal: 'Vision & image generation', harnessProviders: 'Harness Providers', providers: 'Multimodal adapters', jobs: 'Task status' },
197
- openHarness: 'Open 3210 Crew Harness →',
198
- harnessHint: 'Low-level providers, Harness Models, and runtime configuration',
203
+ harnessHint: 'The official DeepSeek Harness dsh-crew profile: providers, Harness Models, and runtime configuration',
199
204
  hostReadiness: 'Host integration readiness', hostReadinessHint: 'Uses structured installer and runtime evidence only; missing evidence is never READY.',
200
- readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', zcode_mcp: 'ZCode MCP', crew_harness: 'Crew Harness', official_bridge: 'Official bridge' },
201
- readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN' },
205
+ readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', zcode_mcp: 'ZCode MCP', crew_harness: 'Crew plugin profile', official_bridge: 'Official bridge' },
206
+ readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN', NOT_APPLICABLE: 'N/A' },
202
207
  globalHint: 'Changes save instantly to ~/.config/dsh-crew/config.json; new CC / Codex sessions pick them up as defaults (override per session with /dsh-crew:config).',
203
208
  orchestration: 'Agent orchestration',
204
209
  enableSubagents: 'Enable Subagents',
@@ -244,7 +249,7 @@ const COPY = {
244
249
  presetFlash: 'flash preset', presetPro: 'pro preset',
245
250
  multimodal: 'Multimodal',
246
251
  visionProvider: 'Vision provider', visionModel: 'Vision model', imagegenProvider: 'Image-gen provider',
247
- harnessProviders: 'Harness Providers', harnessProviderHint: 'Live Provider registration and lifecycle in the 3210 Crew Harness; separate from vision / image-gen adapters.',
252
+ harnessProviders: 'Harness Providers', harnessProviderHint: 'Live Provider registration and lifecycle in the official DeepSeek Harness dsh-crew profile on 3210; separate from vision / image-gen adapters.',
248
253
  providerState: (state: string) => `State: ${state}`, providerModels: (count: number) => `${count} models`, providerJobs: (count: number) => `${count} running jobs`,
249
254
  providerOfficialBuiltin: '· official built-in (immutable)', providerSourceUnresolved: '· source unresolved (locked)',
250
255
  providerLayerMigration: 'Legacy profile Providers detected', providerLayerMigrationHint: 'These Providers still exist in the Crew profile/base layer, so the native 3210 page cannot show Delete. Promote them to Harness user settings with an explicit plan; keys are never copied.', providerLayerMigrationPlan: 'Migration plan', providerLayerMigrate: 'Migrate to user layer', providerLayerMigrating: 'Migrating…', providerLayerMigrationConfirm: 'Move this Provider into Harness user settings? A rollback snapshot is saved first; credential values are never copied.', providerLayerRollback: 'Rollback migration',
@@ -300,8 +305,8 @@ const COPY = {
300
305
  save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
301
306
  adaptiveTitle: 'Adaptive model routing (experimental)', adaptiveHint: 'Off by default. Only system-derived candidates may be reordered; explicit model priorities always keep their order. Health uses only process-local success, failure, timeout, and coarse latency observations and resets on restart.',
302
307
  adaptiveEnabled: 'Enable adaptive routing', adaptiveWindow: 'Health window', adaptiveSamples: 'Minimum samples', adaptiveBoundary: 'Effective for the next workflow',
303
- modelActivity: 'Model invocation overview', modelActivityHint: 'Aggregates real invocation evidence from the latest 500 in-memory Hub jobs and shows at most 50 models; prompts, results, and credentials are never stored here.', noModelActivity: 'No real model invocations yet.',
304
- calls: 'Calls', invocationSource: 'Task source', routingSource: 'Routing source', lastCalled: 'Last called', role: 'Role', model: 'Model', never: '—',
308
+ modelActivity: 'Model activity overview', modelActivityHint: 'Aggregates the latest 500 in-memory Hub jobs with real model-activity evidence; each model is counted once per qualifying job (not an inferred turn count). Shows at most 50 models; prompts, results, and credentials are never stored here.', noModelActivity: 'No real model activity yet.',
309
+ calls: 'Jobs', invocationSource: 'Task source', routingSource: 'Routing source', lastCalled: 'Last called', role: 'Role', model: 'Model', never: '—',
305
310
  col: { id: 'job', role: 'role', source: 'source', model: 'model', tier: 'tier', status: 'status', progress: 'progress', tokens: 'tokens ⇅', task: 'task' },
306
311
  working: 'Working…',
307
312
  tips: {
@@ -430,10 +435,12 @@ function CollapsibleSection({ sectionId, title, summary, expanded, onToggle, chi
430
435
  const bodyId = `dsh-crew-section-${sectionId}`;
431
436
  return (
432
437
  <section style={S.sectionShell} data-section-id={sectionId}>
433
- <button type="button" style={S.sectionButton} aria-expanded={expanded} aria-controls={bodyId} onClick={onToggle}>
438
+ <button type="button" className="crew-section-trigger" style={S.sectionButton} aria-expanded={expanded} aria-controls={bodyId} onClick={onToggle}>
434
439
  <span aria-hidden="true" style={{ width: 14, opacity: 0.62, transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 140ms ease' }}>›</span>
435
- <span style={{ fontWeight: 650, fontSize: 13, minWidth: 92 }}>{title}</span>
436
- <span title={summary} style={{ flex: 1, minWidth: 0, fontSize: 11.5, opacity: 0.58, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{summary}</span>
440
+ <span className="crew-section-heading">
441
+ <span className="crew-section-title">{title}</span>
442
+ <span className="crew-section-summary">{summary}</span>
443
+ </span>
437
444
  </button>
438
445
  <div id={bodyId} hidden={!expanded} style={expanded ? S.sectionBody : undefined}>{children}</div>
439
446
  </section>
@@ -463,7 +470,8 @@ function formatTimestamp(value: string | null, locale: string) {
463
470
  function readinessChip(state: string) {
464
471
  const color = state === READINESS_STATES.READY ? '#3fb950'
465
472
  : state === READINESS_STATES.DEGRADED ? '#c98735'
466
- : state === READINESS_STATES.UNAVAILABLE ? '#f85149' : 'inherit';
473
+ : state === READINESS_STATES.UNAVAILABLE ? '#f85149'
474
+ : state === READINESS_STATES.NOT_APPLICABLE ? '#7d8590' : 'inherit';
467
475
  return {
468
476
  fontSize: 10.5, fontWeight: 650, padding: '1px 7px', borderRadius: 99,
469
477
  border: `1px solid ${state === READINESS_STATES.UNKNOWN ? 'rgba(128,128,128,0.35)' : color}`,
@@ -477,8 +485,8 @@ function MinimalCrewPanel({ locale, surface, runtime }: { locale: string; surfac
477
485
  const title = native ? (zh ? 'DSH Crew Runtime' : 'DSH Crew Runtime') : (zh ? 'DSH Crew Surface 未确认' : 'DSH Crew surface not verified');
478
486
  const hint = native
479
487
  ? (zh
480
- ? '这里是隔离的 3210 Crew Harness。请使用 Harness 原生菜单管理 Provider、Harness Models、Agent 预设与底层配置;Crew 编排和宿主集成统一在 3080 管理。'
481
- : 'This is the isolated 3210 Crew Harness. Use the native Harness menus for Providers, Harness Models, Agent presets, and low-level settings; manage Crew orchestration and host integrations on 3080.')
488
+ ? '这里是 DeepSeek 官方 Harness 的专用 dsh-crew profile,DSH Crew 作为插件加载其中。请使用 Harness 原生菜单管理 Provider、Harness Models、Agent 预设与底层配置。'
489
+ : 'This is the dedicated dsh-crew profile of the official DeepSeek Harness, with DSH Crew loaded as a plugin. Use the native Harness menus for Providers, Harness Models, Agent presets, and low-level settings.')
482
490
  : (zh
483
491
  ? '无法用结构化后端证据确认当前界面,因此已保守隐藏 Crew 编排、任务与宿主集成功能。'
484
492
  : 'Structured backend evidence could not identify this surface, so Crew orchestration, jobs, and host integrations are conservatively hidden.');
@@ -495,13 +503,13 @@ function MinimalCrewPanel({ locale, surface, runtime }: { locale: string; surfac
495
503
  {runtimeReady ? `Runtime ${runtime.runtime_version ?? 'READY'}` : 'Runtime UNKNOWN'}
496
504
  </span>
497
505
  <span style={readinessChip(native ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
498
- {native ? '3210 · isolated' : 'surface · unknown'}
506
+ {native ? '3210 · official profile + Crew plugin' : 'surface · unknown'}
499
507
  </span>
500
508
  </div>
501
509
  <a href={CREW_CONTROL_PLANE_URL} target="_blank" rel="noopener noreferrer" style={{
502
510
  ...S.btn, alignSelf: 'flex-start', textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
503
511
  borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.08)',
504
- }}>{zh ? '打开 3080 DSH Crew 控制台 →' : 'Open the 3080 DSH Crew console →'}</a>
512
+ }}>{zh ? '打开官方 Harness 界面(3080) →' : 'Open official Harness (3080) →'}</a>
505
513
  </div>
506
514
  </div>
507
515
  );
@@ -544,8 +552,18 @@ function WorkersPanel({ ctx }: { ctx: any }) {
544
552
  const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
545
553
  const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
546
554
  const [surface, setSurface] = useState<string>('detecting');
547
- const [runtimeInfo, setRuntimeInfo] = useState<any>(undefined);
548
- const [readinessSnapshot, setReadinessSnapshot] = useState<any>(undefined);
555
+ const [readinessEnvelope, setReadinessEnvelope] = useState<{ runtime?: any; snapshot?: any; expiresAt?: number | null }>({});
556
+ const readinessGeneration = useRef(0);
557
+ const runtimeInfo = readinessEnvelope.runtime;
558
+ const readinessSnapshot = readinessEnvelope.snapshot;
559
+
560
+ useEffect(() => {
561
+ const delay = readinessExpiryDelay(readinessEnvelope.expiresAt);
562
+ if (delay === null) return undefined;
563
+ const expected = readinessEnvelope;
564
+ const timer = setTimeout(() => setReadinessEnvelope((current) => current === expected ? {} : current), delay);
565
+ return () => clearTimeout(timer);
566
+ }, [readinessEnvelope]);
549
567
 
550
568
  const toggleSection = (sectionId: string) => {
551
569
  setExpandedSections((current) => ({ ...current, [sectionId]: !current[sectionId] }));
@@ -594,7 +612,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
594
612
  /** Every request carries the active locale: the panel is the authority on it
595
613
  * (DSH's setting may be unset), and the hub localizes its own strings from it. */
596
614
  const withLang = useCallback((path: string) => `${API}${path}${path.includes('?') ? '&' : '?'}lang=${locale === 'zh' ? 'zh' : 'en'}`, [locale]);
597
- const get = useCallback(async (path: string) => readJson(await fetch(withLang(path), { cache: 'no-store' }), path), [readJson, withLang]);
615
+ const get = useCallback(async (path: string, timeoutMs = 8_000) => readJson(await fetch(withLang(path), { cache: 'no-store', signal: AbortSignal.timeout(timeoutMs) }), path), [readJson, withLang]);
598
616
  const post = useCallback(async (path: string, body: any) => readJson(await fetch(withLang(path), {
599
617
  method: 'POST', headers: { 'content-type': 'application/json' },
600
618
  body: JSON.stringify({ ...body, lang: locale === 'zh' ? 'zh' : 'en' }),
@@ -613,17 +631,18 @@ function WorkersPanel({ ctx }: { ctx: any }) {
613
631
  readOptional('/bridge-status'),
614
632
  readOptional('/runtime'),
615
633
  ]);
616
- setRuntimeInfo(runtime);
617
634
  setSurface(classifyCrewSurface({ bridgeStatus, runtime }));
618
635
  }, []);
619
636
 
620
637
  useEffect(() => { void detectSurface(); }, [detectSurface]);
621
638
 
622
639
  const refreshAll = useCallback(async () => {
640
+ const generation = ++readinessGeneration.current;
641
+ setReadinessEnvelope({});
623
642
  try {
624
- const [j, s, c, pr, pi, ph, cr, ext] = await Promise.all([
643
+ const [j, s, c, pr, pi, ph, cr] = await Promise.all([
625
644
  get('/jobs'), get('/install/status'), get('/config'), get('/presets'),
626
- get('/providers').catch(() => null), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null), get('/extension').catch(() => null),
645
+ get('/providers').catch(() => null), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null),
627
646
  ]);
628
647
  if (j.ok) setJobs(j.jobs ?? []);
629
648
  if (s.ok) setStatus(s.status);
@@ -635,21 +654,37 @@ function WorkersPanel({ ctx }: { ctx: any }) {
635
654
  if (pi?.ok) setProviderInventory(pi);
636
655
  if (ph?.ok) setProviderHealth(ph.health ?? []);
637
656
  if (cr?.ok) { setCredentialRefs(cr.records ?? []); setCredentialUnverified(cr.unverified_purges ?? []); }
638
- if (ext?.ok) setReadinessSnapshot(ext.extension?.readiness_snapshot ?? undefined);
639
- } catch { /* instance restarting */ }
657
+ } catch {
658
+ if (generation === readinessGeneration.current) setReadinessEnvelope({});
659
+ }
640
660
  }, [get]);
641
661
 
642
662
  useEffect(() => {
643
663
  const responsibilities = surfaceResponsibilities(surface);
644
664
  if (!responsibilities.fullControlPlane) return undefined;
645
665
  void refreshAll();
646
- const timer = setInterval(() => {
647
- void Promise.all([get('/jobs'), get('/provider-health').catch(() => null)]).then(([j, health]) => {
648
- if (j.ok) setJobs(j.jobs ?? []);
666
+ let cancelled = false;
667
+ let timer: ReturnType<typeof setTimeout> | undefined;
668
+ const poll = async () => {
669
+ const generation = ++readinessGeneration.current;
670
+ try {
671
+ const ext = await get('/extension', 5_000);
672
+ if (!cancelled && generation === readinessGeneration.current) {
673
+ const accepted = acceptReadinessResponse(ext, { generation, latestGeneration: readinessGeneration.current });
674
+ if (accepted.accepted) setReadinessEnvelope(accepted.envelope);
675
+ }
676
+ } catch {
677
+ if (!cancelled && generation === readinessGeneration.current) setReadinessEnvelope({});
678
+ }
679
+ void Promise.all([get('/jobs', 5_000).catch(() => null), get('/provider-health', 5_000).catch(() => null)]).then(([j, health]) => {
680
+ if (cancelled) return;
681
+ if (j?.ok) setJobs(j.jobs ?? []);
649
682
  if (health?.ok) setProviderHealth(health.health ?? []);
650
683
  }).catch(() => {});
651
- }, 3000);
652
- return () => clearInterval(timer);
684
+ if (!cancelled) timer = setTimeout(() => { void poll(); }, 3_000);
685
+ };
686
+ void poll();
687
+ return () => { cancelled = true; readinessGeneration.current += 1; if (timer) clearTimeout(timer); };
653
688
  }, [surface, refreshAll, get]);
654
689
 
655
690
  const refreshHarnessModels = useCallback(async () => {
@@ -1060,7 +1095,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1060
1095
  <div style={S.card}>
1061
1096
  <span style={{ fontWeight: 600, fontSize: 13, minWidth: 92 }}>{name}</span>
1062
1097
  <span style={S.chip(installed)}>{installed ? `● ${copy.installed}` : `○ ${copy.notInstalled}`}</span>
1063
- <span style={S.chip(ready)}>{ready ? `● ${copy.ready}` : `○ ${copy.notReady}`}</span>
1098
+ <span style={S.chip(ready)}>{ready ? `● ${copy.integrationReady}` : `○ ${copy.notReady}`}</span>
1064
1099
  {extra}
1065
1100
  <span style={{ flex: 1 }} />
1066
1101
  {!installed && <button style={S.btn} title={tips.install} disabled={busy} onClick={() => { void act(installTarget); }}>{copy.install}</button>}
@@ -1087,6 +1122,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1087
1122
  void applyPatch({ worker: { model_policy: { adaptive: next, ordering } } });
1088
1123
  };
1089
1124
  const modelActivity = aggregateModelInvocations(jobs);
1125
+ const modelState = modelCallabilityState(readinessSnapshot?.model_callability, runtimeInfo);
1090
1126
  const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
1091
1127
  const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface, readinessSnapshot });
1092
1128
 
@@ -1095,39 +1131,25 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1095
1131
  }
1096
1132
 
1097
1133
  return (
1098
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 13, lineHeight: 1.55 }}>
1099
- <div style={{
1100
- border: '1px solid rgba(128,128,128,0.24)', borderRadius: 12, padding: '14px 15px',
1101
- display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap',
1102
- background: 'linear-gradient(135deg, rgba(74,158,255,0.10), rgba(128,128,128,0.025) 56%)',
1103
- }}>
1104
- <div style={{ minWidth: 240, flex: 1 }}>
1105
- <div style={{ fontSize: 20, fontWeight: 680, letterSpacing: '-0.01em' }}>{copy.title}</div>
1106
- <div style={{ opacity: 0.7, fontSize: 12.5, marginTop: 2 }}>{copy.intro}</div>
1107
- <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
1108
- <span style={S.chip(!!status?.codex?.ready)}>Codex {status?.codex?.ready ? 'READY' : 'CHECK'}</span>
1109
- <span style={S.chip(!!status?.claude?.ready)}>Claude {status?.claude?.ready ? 'READY' : 'CHECK'}</span>
1110
- <span style={S.chip(!!status?.zcode?.ready)}>ZCode {status?.zcode?.ready ? 'READY' : 'CHECK'}</span>
1111
- <span style={S.chip(jobs.some((job) => job.status === 'running'))}>{jobs.filter((job) => job.status === 'running').length} running</span>
1112
- </div>
1113
- </div>
1114
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 3 }}>
1115
- <a href={CREW_HARNESS_URL} target="_blank" rel="noopener noreferrer" style={{
1116
- ...S.btn, textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
1117
- borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.10)',
1118
- }}>{copy.openHarness}</a>
1119
- <span style={{ fontSize: 10.5, opacity: 0.52 }}>{copy.harnessHint}</span>
1120
- </div>
1121
- </div>
1134
+ <div className="dsh-crew-ui" style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 13, lineHeight: 1.55 }}>
1135
+ <PanelStyles />
1136
+ <PanelHeader title={copy.title} eyebrow={copy.surfaceLabel} description={copy.intro} href={CREW_CONTROL_PLANE_URL} linkText={copy.openOfficial}>
1137
+ <span style={S.chip(status?.codex?.installed === true)}>{`Codex ${status?.codex?.installed === true ? copy.installed : copy.notInstalled}`}</span>
1138
+ <span style={S.chip(status?.claude?.installed === true)}>{`Claude ${status?.claude?.installed === true ? copy.installed : copy.notInstalled}`}</span>
1139
+ <span style={S.chip(status?.zcode?.installed === true)}>{`ZCode ${status?.zcode?.installed === true ? copy.installed : copy.notInstalled}`}</span>
1140
+ <span style={readinessChip(modelState)}>{modelState === READINESS_STATES.READY ? copy.modelCallable : modelState === READINESS_STATES.UNKNOWN ? copy.modelUnknown : copy.modelNotCallable}</span>
1141
+ <span style={S.chip(jobs.some((job) => job.status === 'running'))}>{copy.runningCount(jobs.filter((job) => job.status === 'running').length)}</span>
1142
+ </PanelHeader>
1143
+ <HistoryPanel locale={locale} />
1122
1144
 
1123
- <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 5 }}>
1145
+ <div className="crew-config-toolbar">
1124
1146
  <div style={{ ...S.section, margin: 0, flex: 1 }}>{copy.globalConfig}</div>
1125
1147
  <button type="button" style={{ ...S.btn, padding: '3px 9px' }} onClick={() => toggleAllSections(true)}>{copy.expandAll}</button>
1126
1148
  <button type="button" style={{ ...S.btn, padding: '3px 9px' }} onClick={() => toggleAllSections(false)}>{copy.collapseAll}</button>
1127
1149
  </div>
1128
1150
 
1129
1151
  <CollapsibleSection sectionId="integrations" title={copy.sectionNames.integrations}
1130
- summary={sectionSummary(status?.claude?.ready ? 'Claude READY' : 'Claude CHECK', `${status?.codex?.ready ? 'Codex READY' : 'Codex CHECK'} · ${status?.zcode?.ready ? 'ZCode READY' : 'ZCode CHECK'}`)}
1152
+ summary={sectionSummary(...['Codex', 'Claude', 'ZCode'].map((name) => `${name} ${status?.[name.toLowerCase()]?.installed ? copy.installed : copy.notInstalled}`), modelState === READINESS_STATES.READY ? copy.modelCallable : copy.modelNotCallable)}
1131
1153
  expanded={!!expandedSections.integrations} onToggle={() => toggleSection('integrations')}>
1132
1154
  <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1133
1155
  <div style={S.block}>
@@ -1816,7 +1838,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1816
1838
  <div role="row" key={`${entry.provider}/${entry.model}`} style={{ display: 'grid', gridTemplateColumns: 'minmax(190px, 1.35fr) 72px minmax(170px, 1fr) 125px', gap: 10, alignItems: 'center', padding: '7px 6px', borderTop: '1px solid rgba(128,128,128,0.16)' }}>
1817
1839
  <span role="cell" style={{ minWidth: 0 }}><strong style={{ display: 'block', fontSize: 12 }}>{entry.model}</strong><span style={{ ...S.mono, opacity: 0.55 }}>{entry.provider}</span></span>
1818
1840
  <span role="cell" style={{ ...S.mono, fontSize: 15, fontWeight: 650 }}>{entry.count}</span>
1819
- <span role="cell" style={{ minWidth: 0, fontSize: 11.5 }}><span style={{ display: 'block' }}>{entry.task_sources.map(sourceLabel).join(' · ')}</span><span style={{ display: 'block', opacity: 0.55 }}>{entry.selection_sources.join(' · ')} · {entry.roles.join(' / ')}</span></span>
1841
+ <span role="cell" style={{ minWidth: 0, fontSize: 11.5 }}><span style={{ display: 'block' }}>{entry.task_sources.map(sourceLabel).join(' · ')}</span><span style={{ display: 'block', opacity: 0.55 }}>{entry.selection_sources.join(' · ')} · {entry.roles.length > 0 ? entry.roles.join(' / ') : copy.never}</span></span>
1820
1842
  <span role="cell" style={{ ...S.mono, fontSize: 10.5 }}>{formatTimestamp(entry.last_called_at, locale)}</span>
1821
1843
  </div>
1822
1844
  ))}
@@ -0,0 +1,17 @@
1
+ // Pure client projection for the server-owned model_callability contract.
2
+ // The client must never infer current callability from historical matrix rows.
3
+
4
+ import { READINESS_STATES } from './host-readiness.mjs';
5
+ import { validateModelCallabilityV2 } from '../model-callability-contract.mjs';
6
+
7
+ export function modelCallabilityState(projection, runtime = null, now = Date.now()) {
8
+ if (!projection || typeof projection !== 'object' || typeof projection.overall !== 'string') {
9
+ return READINESS_STATES.UNKNOWN;
10
+ }
11
+ const validation = validateModelCallabilityV2({ projection, runtime, now });
12
+ if (!validation.ok) return READINESS_STATES.UNKNOWN;
13
+ if (validation.state === 'CALLABLE') return READINESS_STATES.READY;
14
+ if (validation.state === 'NOT_CALLABLE') return READINESS_STATES.UNAVAILABLE;
15
+ if (validation.state === 'STALE') return READINESS_STATES.DEGRADED;
16
+ return READINESS_STATES.UNKNOWN;
17
+ }
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ // Scoped to Crew panels: the official application's settings shell is untouched.
4
+ export function PanelStyles() {
5
+ return <style>{`
6
+ .dsh-crew-ui { --crew-line: rgba(128,128,128,.24); --crew-soft: rgba(128,128,128,.045); --crew-accent: #2563eb; font-size: 13px; line-height: 1.55; min-width: 0; }
7
+ .dsh-crew-ui *, .dsh-crew-ui *::before, .dsh-crew-ui *::after { box-sizing: border-box; }
8
+ .dsh-crew-ui .crew-panel-header { border: 1px solid var(--crew-line); border-radius: 12px; padding: 16px; background: var(--crew-soft); display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 16px; }
9
+ .dsh-crew-ui .crew-panel-heading { flex: 1 1 260px; min-width: 0; }
10
+ .dsh-crew-ui .crew-eyebrow { font-size: 10.5px; letter-spacing: .055em; opacity: .65; margin-bottom: 4px; }
11
+ .dsh-crew-ui .crew-panel-title { margin: 0; font-size: 19px; font-weight: 680; line-height: 1.3; letter-spacing: -.025em; }
12
+ .dsh-crew-ui .crew-panel-description { margin: 5px 0 0; font-size: 12px; opacity: .72; overflow-wrap: anywhere; }
13
+ .dsh-crew-ui .crew-panel-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
14
+ .dsh-crew-ui .crew-nav-link { display: inline-flex; align-items: center; justify-content: center; color: var(--crew-accent); background: transparent; border: 1px solid var(--crew-line); border-radius: 8px; padding: 7px 10px; font-size: 12px; font-weight: 600; text-decoration: none; flex-shrink: 0; }
15
+ .dsh-crew-ui button:focus-visible, .dsh-crew-ui a:focus-visible, .dsh-crew-ui input:focus-visible, .dsh-crew-ui summary:focus-visible { outline: 2px solid var(--crew-accent); outline-offset: 3px; }
16
+ .dsh-crew-ui button:disabled { cursor: not-allowed; opacity: .4; }
17
+ .dsh-crew-ui .crew-nav-link:hover, .dsh-crew-ui .crew-section-trigger:hover, .dsh-crew-ui summary:hover { background: rgba(128,128,128,.09); }
18
+ .dsh-crew-ui .crew-section-heading { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
19
+ .dsh-crew-ui .crew-section-title { font-weight: 650; font-size: 13px; }
20
+ .dsh-crew-ui .crew-section-summary { font-size: 11.5px; opacity: .65; overflow-wrap: anywhere; }
21
+ .dsh-crew-ui .crew-config-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin: 6px 0 2px; }
22
+ @media (prefers-color-scheme: dark) { .dsh-crew-ui { --crew-accent: #8ab4ff; } }
23
+ @media (prefers-reduced-motion: reduce) { .dsh-crew-ui * { transition: none !important; } }
24
+ `}</style>;
25
+ }
26
+
27
+ export function PanelHeader({ title, eyebrow, description, href, linkText, children }: {
28
+ title: string; eyebrow: string; description: string; href: string;
29
+ linkText: string; children?: ReactNode;
30
+ }) {
31
+ return <header className="crew-panel-header">
32
+ <div className="crew-panel-heading">
33
+ <div className="crew-eyebrow">{eyebrow}</div>
34
+ <h2 className="crew-panel-title">{title}</h2>
35
+ <p className="crew-panel-description">{description}</p>
36
+ {children && <div className="crew-panel-badges">{children}</div>}
37
+ </div>
38
+ <a className="crew-nav-link" href={href} target="_blank" rel="noopener noreferrer">{linkText}</a>
39
+ </header>;
40
+ }