@ran-sh/dsh-crew 1.0.1 → 1.0.3

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.
@@ -4,60 +4,60 @@
4
4
 
5
5
  import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
- import {
7
+ import {
8
8
  openSections,
9
9
  readSectionState,
10
10
  setEverySection,
11
11
  writeSectionState,
12
- } from './collapsible-sections.mjs';
13
- import { ActivationSummary } from './activation-summary';
14
- import { projectHostReadiness, READINESS_STATES } from './host-readiness.mjs';
15
- import { CREW_UI_SURFACES, classifyCrewSurface, surfaceResponsibilities } from './surface-detection.mjs';
16
- import { aggregateModelInvocations } from './task-telemetry.mjs';
12
+ } from './collapsible-sections.mjs';
13
+ import { ActivationSummary } from './activation-summary';
14
+ import { projectHostReadiness, READINESS_STATES } from './host-readiness.mjs';
15
+ import { CREW_UI_SURFACES, classifyCrewSurface, surfaceResponsibilities } from './surface-detection.mjs';
16
+ import { aggregateModelInvocations } from './task-telemetry.mjs';
17
17
 
18
18
  export const inject = ['slots', 'locale'];
19
19
 
20
- const API = '/_dsh/dsh-crew';
21
- const CREW_HARNESS_URL = 'http://127.0.0.1:3210/';
22
- const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3080/';
23
-
24
- type AdaptiveConfig = { enabled: boolean; window_size: number; min_samples: number };
25
- const DEFAULT_ADAPTIVE: AdaptiveConfig = { enabled: false, window_size: 8, min_samples: 2 };
26
-
27
- function clampInt(value: unknown, fallback: number, min: number, max: number) {
28
- const parsed = Number(value);
29
- if (!Number.isInteger(parsed)) return fallback;
30
- return Math.max(min, Math.min(max, parsed));
31
- }
32
-
33
- function normalizeAdaptive(value: any): AdaptiveConfig {
34
- const windowSize = clampInt(value?.window_size, DEFAULT_ADAPTIVE.window_size, 1, 32);
35
- return {
36
- enabled: value?.enabled === true,
37
- window_size: windowSize,
38
- min_samples: clampInt(value?.min_samples, DEFAULT_ADAPTIVE.min_samples, 1, windowSize),
39
- };
40
- }
20
+ const API = '/_dsh/dsh-crew';
21
+ const CREW_HARNESS_URL = 'http://127.0.0.1:3210/';
22
+ const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3080/';
23
+
24
+ type AdaptiveConfig = { enabled: boolean; window_size: number; min_samples: number };
25
+ const DEFAULT_ADAPTIVE: AdaptiveConfig = { enabled: false, window_size: 8, min_samples: 2 };
26
+
27
+ function clampInt(value: unknown, fallback: number, min: number, max: number) {
28
+ const parsed = Number(value);
29
+ if (!Number.isInteger(parsed)) return fallback;
30
+ return Math.max(min, Math.min(max, parsed));
31
+ }
32
+
33
+ function normalizeAdaptive(value: any): AdaptiveConfig {
34
+ const windowSize = clampInt(value?.window_size, DEFAULT_ADAPTIVE.window_size, 1, 32);
35
+ return {
36
+ enabled: value?.enabled === true,
37
+ window_size: windowSize,
38
+ min_samples: clampInt(value?.min_samples, DEFAULT_ADAPTIVE.min_samples, 1, windowSize),
39
+ };
40
+ }
41
41
 
42
42
  const COPY = {
43
43
  zh: {
44
44
  label: 'DSH Crew',
45
45
  title: 'DSH Crew',
46
- intro: '日常 Crew 控制台:在一个入口里配置编排、角色、模型路由与宿主集成,并跟踪真实 Worker / Reviewer 任务。',
46
+ intro: '日常 Crew 控制台:在一个入口里配置编排、角色、模型路由与宿主集成,并跟踪真实 Worker / Reviewer 任务。',
47
47
  integrations: '集成',
48
- installed: '已安装', notInstalled: '未安装', ready: '可调用', notReady: '未就绪', hud: 'HUD 段',
48
+ installed: '已安装', notInstalled: '未安装', ready: '可调用', notReady: '未就绪', hud: 'HUD 段',
49
49
  install: '安装', update: '更新', restore: '还原',
50
50
  confirmRestore: (name: string) => `确定从 ${name} 移除 dsh-crew 集成?(settings 会先备份)`,
51
51
  globalConfig: '全局配置',
52
52
  expandAll: '全部展开', collapseAll: '全部折叠',
53
53
  modelCount: (count: number) => `${count} 个模型`, providerCount: (count: number) => `${count} 个 Provider`,
54
54
  jobCount: (count: number) => `${count} 个任务`, runningCount: (count: number) => `${count} 个运行中`,
55
- 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 与运行时配置',
58
- 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' },
55
+ 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 与运行时配置',
58
+ 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' },
61
61
  globalHint: '修改即时保存到 ~/.config/dsh-crew/config.json;CC / Codex 的新会话自动读取为默认值(会话内可用 /dsh-crew:config 临时覆盖)。',
62
62
  orchestration: 'Agent 编排',
63
63
  enableSubagents: '启用子 Agent',
@@ -75,12 +75,12 @@ const COPY = {
75
75
  statePresetHint: '切到 Custom 可单独配置各档状态。',
76
76
  workerProvider: 'Worker Provider',
77
77
  workerProviderDesc: { 'follow-dsh': '跟随 Harness · 使用各档独立的模型优先级', 'deepseek-official': 'DeepSeek Official · 使用旧版固定模型路由' },
78
- workerProviderHint: '仅作用于 Hub 模式;Standalone 仅保留为旧版迁移元数据,不是生产执行路径。',
78
+ workerProviderHint: '仅作用于 Hub 模式;Standalone 仅保留为旧版迁移元数据,不是生产执行路径。',
79
79
  workerModels: 'Worker 模型', refreshHarnessModels: '刷新 Harness 模型', refreshingModels: '刷新中…',
80
80
  modelPriority: '模型优先级', addPriorityModel: '+ 添加优先模型', modelSearch: '搜索 Provider 或 Model…',
81
81
  harnessDefault: 'Harness 默认模型', providerUnavailable: 'Provider 不可用', notAdvertised: '当前未列出',
82
- noPriority: '未配置优先项', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
83
- healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `${s} 秒前检查`,
82
+ noPriority: '未配置优先项', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
83
+ healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `${s} 秒前检查`,
84
84
  partialCatalog: '部分 Provider 读取失败', catalogFailed: '无法读取 Harness 模型目录', removePriority: '移除', moveUp: '上移', moveDown: '下移',
85
85
  needsModelSelection: '多个 Provider 提供默认偏好模型,且 Harness Default 无法消歧;当前将回退到 Harness Default,请手动选择。',
86
86
  responsibilities: '职责',
@@ -101,17 +101,19 @@ const COPY = {
101
101
  tierPolicyDesc: { auto: 'auto · orchestrator 自选', 'flash-only': '只用 flash', 'pro-only': '只用 pro' },
102
102
  escalateHint: 'flash 失败自动用 pro 重试一次',
103
103
  presetFlash: 'flash 模式', presetPro: 'pro 模式',
104
- multimodal: '多模态',
105
- visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
106
- harnessProviders: 'Harness Providers', harnessProviderHint: '3210 Crew Harness 的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。',
107
- providerState: (state: string) => `状态:${state}`, providerModels: (count: number) => `${count} 个模型`, providerJobs: (count: number) => `${count} 个运行任务`,
108
- providerDeletePlan: '生成删除计划', providerDelete: '确认删除', providerPlanReady: '计划已生成;再次确认后才会修改 3210 配置。', providerRollback: '回滚', providerRollbackConfirm: '按已保存事务恢复这个 Harness Provider 及原有路由?', providerRollbackUnavailable: '没有可验证的回滚事务',
109
- providerReplacement: '替换 Harness 默认 Provider(输入 Provider id)', providerDeleteConfirm: '确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。',
110
- providerDeleteBlocked: '当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。', providerLifecycleError: 'Provider 生命周期操作失败',
111
- providerNoInventory: '暂时无法读取 3210 Provider inventory。',
112
- credentialReferences: 'Credential 引用', credentialReferenceHint: '仅显示引用名、归属和孤儿状态;不会读取或删除密钥。',
113
- credentialOrphan: '孤儿 · 可单独申请清理', credentialInUse: (count: number) => `使用中 · ${count} 个 Provider`, credentialPurgePlan: '申请清理', credentialPurgeConfirm: '这是不可恢复的 Crew-owned 凭据清理,确认继续?', credentialPurging: '清理中…', credentialPurgeUnavailable: '凭据清理不可用', credentialPurgeUnverified: '已执行清理,但验证失败;引用仍保留用于恢复检查',
114
- customProviders: '自定义 Provider', addProvider: '+ 添加 Provider',
104
+ multimodal: '多模态',
105
+ visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
106
+ harnessProviders: 'Harness Providers', harnessProviderHint: '3210 Crew Harness 的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。',
107
+ providerState: (state: string) => `状态:${state}`, providerModels: (count: number) => `${count} 个模型`, providerJobs: (count: number) => `${count} 个运行任务`,
108
+ providerOfficialBuiltin: '· 官方内置(不可删除)', providerSourceUnresolved: '· 来源未解析(安全锁定)',
109
+ providerLayerMigration: '检测到旧版 profile Provider', providerLayerMigrationHint: '这些 Provider 仍在 Crew profile/base 层,所以 3210 原生页面不会显示删除。请先按迁移计划提升到 Harness 用户设置层;不会复制密钥。', providerLayerMigrationPlan: '迁移计划', providerLayerMigrate: '迁移到用户层', providerLayerMigrating: '迁移中…', providerLayerMigrationConfirm: '确认把这个 Provider 移到 Harness 用户设置层?会先保存可回滚快照,不会复制密钥值。', providerLayerRollback: '回滚迁移',
110
+ providerDeletePlan: '生成删除计划', providerDelete: '确认删除', providerPlanReady: '计划已生成;再次确认后才会修改 3210 配置。', providerRollback: '回滚', providerRollbackConfirm: '按已保存事务恢复这个 Harness Provider 及原有路由?', providerRollbackUnavailable: '没有可验证的回滚事务',
111
+ providerReplacement: '替换 Harness 默认 Provider(输入 Provider id)', providerDeleteConfirm: '确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。',
112
+ providerDeleteBlocked: '当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。', providerLifecycleError: 'Provider 生命周期操作失败', providerDeletePending: '配置可能已变更,事务仍待重启/验证;请刷新状态后选择回滚。', providerRollbackContinue: '继续回滚验证', recoveryUnresolved: '发现不可自动恢复的事务', recoveryStorage: '存储项', recoveryQuarantine: '隔离', recoveryQuarantineConfirm: '该事务清单无法安全恢复。将其移入隔离区并阻止自动使用,继续?',
113
+ providerNoInventory: '暂时无法读取 3210 Provider inventory。',
114
+ credentialReferences: 'Credential 引用', credentialReferenceHint: '仅显示引用名、归属和孤儿状态;不会读取或删除密钥。',
115
+ credentialOrphan: '孤儿 · 可单独申请清理', credentialInUse: (count: number) => `使用中 · ${count} 个 Provider`, credentialPurgePlan: '申请清理', credentialPurgeConfirm: '这是不可恢复的 Crew-owned 凭据清理,确认继续?', credentialPurging: '清理中…', credentialPurgeUnavailable: '凭据清理不可用', credentialPurgeUnverified: '已执行清理,但验证失败;引用仍保留用于恢复检查',
116
+ customProviders: '多模态适配器', addProvider: '+ 添加多模态适配器',
115
117
  noCustomProviders: '暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。',
116
118
  providerName: '名称', modelsField: '模型列表(逗号分隔)',
117
119
  providerType: '接入方式', typeApi: 'API · 兼容 OpenAI 接口', typeCli: 'CLI · 本地命令',
@@ -140,11 +142,11 @@ const COPY = {
140
142
  roleWorker: 'Worker 角色(执行)', roleReviewer: 'Reviewer 角色(独立审查)',
141
143
  workerStateLabel: 'Worker 状态', reviewStateLabel: 'Reviewer 状态',
142
144
  roleStateHint: 'auto:orchestrator 可自动派发;manual:仅显式点名;disabled:禁用。未显式设置时由协作模式推导。',
143
- autoReview: '自动复审', autoReviewHint: 'worker 成功后自动运行一次 reviewer(独立模型策略,只读)',
144
- modelOrdering: '候选排序', orderingDesc: { manual: '手动顺序', 'health-aware': '健康感知' },
145
- orderingHint: '手动 priority 始终保持顺序;健康感知仅重排未显式锁定的自动候选。',
146
- healthGate: '健康门', healthGateDesc: { 'hard-failures': '跳过明确不可调用', off: '关闭健康门' },
147
- reviewGate: 'Reviewer 验收门', reviewGateDesc: { required: 'required · 未批准则失败', optional: 'optional · 记录但不阻断', off: 'off · 不自动复审' },
145
+ autoReview: '自动复审', autoReviewHint: 'worker 成功后自动运行一次 reviewer(独立模型策略,只读)',
146
+ modelOrdering: '候选排序', orderingDesc: { manual: '手动顺序', 'health-aware': '健康感知' },
147
+ orderingHint: '手动 priority 始终保持顺序;健康感知仅重排未显式锁定的自动候选。',
148
+ healthGate: '健康门', healthGateDesc: { 'hard-failures': '跳过明确不可调用', off: '关闭健康门' },
149
+ reviewGate: 'Reviewer 验收门', reviewGateDesc: { required: 'required · 未批准则失败', optional: 'optional · 记录但不阻断', off: 'off · 不自动复审' },
148
150
  isolation: '工作区隔离', isolationHint: 'worktree 为默认;非 Git 仓库时需显式 shared,否则任务以 NOT_GIT_REPOSITORY 失败。',
149
151
  isolationDesc: { worktree: 'worktree · 每个 coding worker 独立临时 git worktree(主工作区不动)', shared: 'shared · 旧版就地执行' },
150
152
  maxParallel: '最大并行', maxParallelHint: '同时运行的工作流上限(1–16);超出进入队列。',
@@ -153,13 +155,13 @@ const COPY = {
153
155
  cardRuntime: { t: '执行与连接', d: 'worker 会话跑在本实例内还是独立进程,单个任务的超时上限,以及 CC / Codex 探测本实例的地址。' },
154
156
  cardMM: { t: '视觉与生图', d: '给纯文本的 DSH 模型借来眼睛和画笔:describe_image、会话贴图转写与 generate_image 都用这里的设置。' },
155
157
  cardCustomProv: { t: '自定义 Provider', d: '接入自己的 API 或本地命令;保存后会出现在上面的视觉 / 生图 provider 选择里。' },
156
- modeDesc: { auto: 'auto · 优先 3210', hub: 'hub · 必须 3210' },
157
- save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
158
- adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
159
- adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
160
- modelActivity: '模型调用概览', modelActivityHint: '聚合本 Hub 内存中最近 500 个任务的真实调用证据,最多显示 50 个模型;不保存提示词、结果或凭据。', noModelActivity: '还没有真实模型调用。',
161
- calls: '调用次数', invocationSource: '任务来源', routingSource: '路由来源', lastCalled: '最近调用', role: '角色', model: '模型', never: '—',
162
- col: { id: '任务', role: '角色', source: '来源', model: '模型', tier: '档位', status: '状态', progress: '进度', tokens: 'tokens ⇅', task: '内容' },
158
+ modeDesc: { auto: 'auto · 优先 3210', hub: 'hub · 必须 3210' },
159
+ save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
160
+ adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
161
+ adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
162
+ modelActivity: '模型调用概览', modelActivityHint: '聚合本 Hub 内存中最近 500 个任务的真实调用证据,最多显示 50 个模型;不保存提示词、结果或凭据。', noModelActivity: '还没有真实模型调用。',
163
+ calls: '调用次数', invocationSource: '任务来源', routingSource: '路由来源', lastCalled: '最近调用', role: '角色', model: '模型', never: '—',
164
+ col: { id: '任务', role: '角色', source: '来源', model: '模型', tier: '档位', status: '状态', progress: '进度', tokens: 'tokens ⇅', task: '内容' },
163
165
  working: '处理中…',
164
166
  tips: {
165
167
  claude: {
@@ -167,36 +169,36 @@ const COPY = {
167
169
  update: '重跑安装流程:刷新注册与权限、迁移旧格式配置、更新 HUD 接线;幂等,可放心点',
168
170
  restore: '从 Claude Code 移除集成:清除 marketplace 注册、插件启用项与权限规则(settings 先备份);不删除插件源码与已跑任务',
169
171
  },
170
- codex: {
171
- install: '把 ds-flash / ds-pro 角色和 /dsh-config、/dsh-status 命令复制到 ~/.codex/(MCP 路径按本机自动渲染,含 approve 审批与超时配置)',
172
- update: '重新渲染并覆盖角色文件与命令(插件路径或配置变更后用);原文件先备份',
173
- restore: '删除 ~/.codex/ 下的 ds-flash / ds-pro 角色与 dsh 命令文件(角色文件先备份)',
174
- },
175
- zcode: {
176
- install: '安装 ZCode 全局规则、ds-worker / ds-reviewer 角色、命令和按来源选择的 MCP 配置到 ~/.zcode/',
177
- update: '重新渲染 ZCode 角色、命令和 MCP 条目;保留个人文件与其他 MCP 服务',
178
- restore: '仅移除 dsh-crew 管理的 ZCode 文件和 MCP 条目(保留备份)',
179
- },
172
+ codex: {
173
+ install: '把 ds-flash / ds-pro 角色和 /dsh-config、/dsh-status 命令复制到 ~/.codex/(MCP 路径按本机自动渲染,含 approve 审批与超时配置)',
174
+ update: '重新渲染并覆盖角色文件与命令(插件路径或配置变更后用);原文件先备份',
175
+ restore: '删除 ~/.codex/ 下的 ds-flash / ds-pro 角色与 dsh 命令文件(角色文件先备份)',
176
+ },
177
+ zcode: {
178
+ install: '安装 ZCode 全局规则、ds-worker / ds-reviewer 角色、命令和按来源选择的 MCP 配置到 ~/.zcode/',
179
+ update: '重新渲染 ZCode 角色、命令和 MCP 条目;保留个人文件与其他 MCP 服务',
180
+ restore: '仅移除 dsh-crew 管理的 ZCode 文件和 MCP 条目(保留备份)',
181
+ },
180
182
  },
181
183
  },
182
184
  en: {
183
185
  label: 'DSH Crew',
184
186
  title: 'DSH Crew',
185
- intro: 'The daily Crew console: configure orchestration, roles, model routing, and host integrations in one place while tracking real Worker / Reviewer jobs.',
187
+ intro: 'The daily Crew console: configure orchestration, roles, model routing, and host integrations in one place while tracking real Worker / Reviewer jobs.',
186
188
  integrations: 'Integrations',
187
- installed: 'installed', notInstalled: 'not installed', ready: 'ready', notReady: 'not ready', hud: 'HUD segment',
189
+ installed: 'installed', notInstalled: 'not installed', ready: 'ready', notReady: 'not ready', hud: 'HUD segment',
188
190
  install: 'Install', update: 'Update', restore: 'Restore',
189
191
  confirmRestore: (name: string) => `Remove the dsh-crew integration from ${name}? (settings are backed up first)`,
190
192
  globalConfig: 'Global configuration',
191
193
  expandAll: 'Expand all', collapseAll: 'Collapse all',
192
194
  modelCount: (count: number) => `${count} models`, providerCount: (count: number) => `${count} providers`,
193
195
  jobCount: (count: number) => `${count} jobs`, runningCount: (count: number) => `${count} running`,
194
- 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' },
195
- openHarness: 'Open 3210 Crew Harness →',
196
- harnessHint: 'Low-level providers, Harness Models, and runtime configuration',
197
- hostReadiness: 'Host integration readiness', hostReadinessHint: 'Uses structured installer and runtime evidence only; missing evidence is never READY.',
198
- 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' },
199
- readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN' },
196
+ 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',
199
+ 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' },
200
202
  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).',
201
203
  orchestration: 'Agent orchestration',
202
204
  enableSubagents: 'Enable Subagents',
@@ -214,12 +216,12 @@ const COPY = {
214
216
  statePresetHint: 'Switch to Custom to configure each tier state separately.',
215
217
  workerProvider: 'Worker Provider',
216
218
  workerProviderDesc: { 'follow-dsh': 'Follow Harness · use each tier\'s ordered model priority', 'deepseek-official': 'DeepSeek Official · use legacy fixed model routing' },
217
- workerProviderHint: 'Applies to Hub workers only; Standalone is legacy migration metadata only and is not a production execution path.',
219
+ workerProviderHint: 'Applies to Hub workers only; Standalone is legacy migration metadata only and is not a production execution path.',
218
220
  workerModels: 'Worker Models', refreshHarnessModels: 'Refresh Harness Models', refreshingModels: 'Refreshing…',
219
221
  modelPriority: 'Model Priority', addPriorityModel: '+ Add Priority Model', modelSearch: 'Search Provider or Model…',
220
222
  harnessDefault: 'Harness Default', providerUnavailable: 'Provider unavailable', notAdvertised: 'Currently not advertised',
221
- noPriority: 'No priority models configured', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
222
- healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `checked ${s}s ago`,
223
+ noPriority: 'No priority models configured', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
224
+ healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `checked ${s}s ago`,
223
225
  partialCatalog: 'Some providers failed to load', catalogFailed: 'Unable to read Harness model catalog', removePriority: 'Remove', moveUp: 'Move up', moveDown: 'Move down',
224
226
  needsModelSelection: 'Multiple providers advertise the preferred model and Harness Default cannot disambiguate them. Falling back to Harness Default; choose a priority model explicitly.',
225
227
  responsibilities: 'Responsibilities',
@@ -240,17 +242,19 @@ const COPY = {
240
242
  tierPolicyDesc: { auto: 'auto · orchestrator picks', 'flash-only': 'flash only', 'pro-only': 'pro only' },
241
243
  escalateHint: 'retry a failed flash run once on pro',
242
244
  presetFlash: 'flash preset', presetPro: 'pro preset',
243
- multimodal: 'Multimodal',
244
- visionProvider: 'Vision provider', visionModel: 'Vision model', imagegenProvider: 'Image-gen provider',
245
- harnessProviders: 'Harness Providers', harnessProviderHint: 'Live Provider registration and lifecycle in the 3210 Crew Harness; separate from vision / image-gen adapters.',
246
- providerState: (state: string) => `State: ${state}`, providerModels: (count: number) => `${count} models`, providerJobs: (count: number) => `${count} running jobs`,
247
- providerDeletePlan: 'Plan deletion', providerDelete: 'Confirm delete', providerPlanReady: 'Plan ready; a second confirmation is required before changing 3210 config.', providerRollback: 'Rollback', providerRollbackConfirm: 'Restore this Harness Provider and its previous routing from the saved transaction?', providerRollbackUnavailable: 'No verified rollback transaction is available',
248
- providerReplacement: 'Replacement Harness Default provider id', providerDeleteConfirm: 'Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.',
249
- providerDeleteBlocked: 'This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).', providerLifecycleError: 'Provider lifecycle operation failed',
250
- providerNoInventory: '3210 Provider inventory is temporarily unavailable.',
251
- credentialReferences: 'Credential references', credentialReferenceHint: 'Names, ownership, and orphan status only; values are never read or deleted.',
252
- credentialOrphan: 'orphan · separate purge approval required', credentialInUse: (count: number) => `in use · ${count} provider(s)`, credentialPurgePlan: 'Purge…', credentialPurgeConfirm: 'This irreversibly removes a Crew-owned credential. Continue?', credentialPurging: 'Purging…', credentialPurgeUnavailable: 'Credential purge unavailable', credentialPurgeUnverified: 'Purge ran but verification failed; the reference remains visible for recovery',
253
- customProviders: 'Custom providers', addProvider: '+ Add provider',
245
+ multimodal: 'Multimodal',
246
+ 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.',
248
+ providerState: (state: string) => `State: ${state}`, providerModels: (count: number) => `${count} models`, providerJobs: (count: number) => `${count} running jobs`,
249
+ providerOfficialBuiltin: '· official built-in (immutable)', providerSourceUnresolved: '· source unresolved (locked)',
250
+ 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',
251
+ providerDeletePlan: 'Plan deletion', providerDelete: 'Confirm delete', providerPlanReady: 'Plan ready; a second confirmation is required before changing 3210 config.', providerRollback: 'Rollback', providerRollbackConfirm: 'Restore this Harness Provider and its previous routing from the saved transaction?', providerRollbackUnavailable: 'No verified rollback transaction is available',
252
+ providerReplacement: 'Replacement Harness Default provider id', providerDeleteConfirm: 'Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.',
253
+ providerDeleteBlocked: 'This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).', providerLifecycleError: 'Provider lifecycle operation failed', providerDeletePending: 'Configuration may already be changed; the transaction still needs restart/verification. Refresh status and use rollback if needed.', providerRollbackContinue: 'Continue rollback verification', recoveryUnresolved: 'An unrecoverable recovery transaction is blocking deletion', recoveryStorage: 'storage entry', recoveryQuarantine: 'Quarantine', recoveryQuarantineConfirm: 'This recovery manifest cannot be safely reopened. Move it into the quarantine area and keep it out of automatic recovery?',
254
+ providerNoInventory: '3210 Provider inventory is temporarily unavailable.',
255
+ credentialReferences: 'Credential references', credentialReferenceHint: 'Names, ownership, and orphan status only; values are never read or deleted.',
256
+ credentialOrphan: 'orphan · separate purge approval required', credentialInUse: (count: number) => `in use · ${count} provider(s)`, credentialPurgePlan: 'Purge…', credentialPurgeConfirm: 'This irreversibly removes a Crew-owned credential. Continue?', credentialPurging: 'Purging…', credentialPurgeUnavailable: 'Credential purge unavailable', credentialPurgeUnverified: 'Purge ran but verification failed; the reference remains visible for recovery',
257
+ customProviders: 'Multimodal adapters', addProvider: '+ Add multimodal adapter',
254
258
  noCustomProviders: 'None yet. Added providers appear in the vision / image-gen selects above.',
255
259
  providerName: 'Name', modelsField: 'Models (comma-separated)',
256
260
  providerType: 'Connection', typeApi: 'API · OpenAI-compatible', typeCli: 'CLI · local command',
@@ -279,11 +283,11 @@ const COPY = {
279
283
  roleWorker: 'Worker role (execution)', roleReviewer: 'Reviewer role (independent review)',
280
284
  workerStateLabel: 'Worker state', reviewStateLabel: 'Reviewer state',
281
285
  roleStateHint: 'auto = the orchestrator may dispatch automatically; manual = only when explicitly named; disabled = off. When unset, derived from the collaboration mode.',
282
- autoReview: 'Automatic review', autoReviewHint: 'Run one reviewer pass (independent model policy, read-only) after a successful worker run',
283
- modelOrdering: 'Candidate ordering', orderingDesc: { manual: 'Manual order', 'health-aware': 'Health-aware' },
284
- orderingHint: 'Explicit priority always keeps its order; health-aware mode only reorders automatic, unlocked candidates.',
285
- healthGate: 'Health gate', healthGateDesc: { 'hard-failures': 'Skip known hard failures', off: 'Health gate off' },
286
- reviewGate: 'Reviewer acceptance gate', reviewGateDesc: { required: 'required · fail unless approved', optional: 'optional · record without blocking', off: 'off · no automatic review' },
286
+ autoReview: 'Automatic review', autoReviewHint: 'Run one reviewer pass (independent model policy, read-only) after a successful worker run',
287
+ modelOrdering: 'Candidate ordering', orderingDesc: { manual: 'Manual order', 'health-aware': 'Health-aware' },
288
+ orderingHint: 'Explicit priority always keeps its order; health-aware mode only reorders automatic, unlocked candidates.',
289
+ healthGate: 'Health gate', healthGateDesc: { 'hard-failures': 'Skip known hard failures', off: 'Health gate off' },
290
+ reviewGate: 'Reviewer acceptance gate', reviewGateDesc: { required: 'required · fail unless approved', optional: 'optional · record without blocking', off: 'off · no automatic review' },
287
291
  isolation: 'Workspace isolation', isolationHint: 'worktree is the default; non-git workspaces require explicit shared, otherwise jobs fail with NOT_GIT_REPOSITORY.',
288
292
  isolationDesc: { worktree: 'worktree · each coding worker runs in its own temporary git worktree (primary untouched)', shared: 'shared · legacy in-place execution' },
289
293
  maxParallel: 'Max parallel', maxParallelHint: 'Upper bound of concurrently running workflows (1–16); extra ones queue.',
@@ -292,13 +296,13 @@ const COPY = {
292
296
  cardRuntime: { t: 'Execution & connection', d: 'Whether worker sessions run inside this instance or a separate process, the per-job timeout, and the address CC / Codex probes.' },
293
297
  cardMM: { t: 'Vision & image generation', d: "Lends the harness's text-only models eyes and a brush: describe_image, pasted-image transcription and generate_image all use these settings." },
294
298
  cardCustomProv: { t: 'Custom providers', d: 'Bring your own API or local command; saved providers appear in the vision / image-gen selects above.' },
295
- modeDesc: { auto: 'auto · prefer 3210', hub: 'hub · require 3210' },
296
- save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
297
- 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.',
298
- adaptiveEnabled: 'Enable adaptive routing', adaptiveWindow: 'Health window', adaptiveSamples: 'Minimum samples', adaptiveBoundary: 'Effective for the next workflow',
299
- 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.',
300
- calls: 'Calls', invocationSource: 'Task source', routingSource: 'Routing source', lastCalled: 'Last called', role: 'Role', model: 'Model', never: '—',
301
- col: { id: 'job', role: 'role', source: 'source', model: 'model', tier: 'tier', status: 'status', progress: 'progress', tokens: 'tokens ⇅', task: 'task' },
299
+ modeDesc: { auto: 'auto · prefer 3210', hub: 'hub · require 3210' },
300
+ save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
301
+ 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
+ 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: '—',
305
+ col: { id: 'job', role: 'role', source: 'source', model: 'model', tier: 'tier', status: 'status', progress: 'progress', tokens: 'tokens ⇅', task: 'task' },
302
306
  working: 'Working…',
303
307
  tips: {
304
308
  claude: {
@@ -306,16 +310,16 @@ const COPY = {
306
310
  update: 'Re-run the installer: refresh registration & permissions, migrate legacy config, update HUD wiring; idempotent',
307
311
  restore: 'Remove the integration from Claude Code: marketplace registration, plugin enablement and permission rules (settings backed up); plugin source and past jobs untouched',
308
312
  },
309
- codex: {
310
- install: 'Copy ds-flash / ds-pro roles and /dsh-config, /dsh-status prompts into ~/.codex/ (MCP paths rendered for this machine, approve mode + timeout included)',
311
- update: 'Re-render and overwrite role files and prompts (after path/config changes); originals backed up',
312
- restore: 'Delete ds-flash / ds-pro roles and dsh prompts from ~/.codex/ (roles backed up first)',
313
- },
314
- zcode: {
315
- install: 'Install the ZCode global policy, ds-worker / ds-reviewer agents, commands and source-aware MCP config under ~/.zcode/',
316
- update: 'Re-render ZCode agents, commands and MCP entry; user files and unrelated MCP servers are preserved',
317
- restore: 'Remove only dsh-crew-owned ZCode files and MCP entry (backups kept)',
318
- },
313
+ codex: {
314
+ install: 'Copy ds-flash / ds-pro roles and /dsh-config, /dsh-status prompts into ~/.codex/ (MCP paths rendered for this machine, approve mode + timeout included)',
315
+ update: 'Re-render and overwrite role files and prompts (after path/config changes); originals backed up',
316
+ restore: 'Delete ds-flash / ds-pro roles and dsh prompts from ~/.codex/ (roles backed up first)',
317
+ },
318
+ zcode: {
319
+ install: 'Install the ZCode global policy, ds-worker / ds-reviewer agents, commands and source-aware MCP config under ~/.zcode/',
320
+ update: 'Re-render ZCode agents, commands and MCP entry; user files and unrelated MCP servers are preserved',
321
+ restore: 'Remove only dsh-crew-owned ZCode files and MCP entry (backups kept)',
322
+ },
319
323
  },
320
324
  },
321
325
  };
@@ -447,63 +451,63 @@ function elapsed(startedAt: string, endedAt?: string | null) {
447
451
  const s = Math.max(0, Math.round(((endedAt ? +new Date(endedAt) : Date.now()) - +new Date(startedAt)) / 1000));
448
452
  return s >= 60 ? `${Math.floor(s / 60)}m${s % 60}s` : `${s}s`;
449
453
  }
450
- function ktok(n: number) { return n >= 1000 ? `${Math.round(n / 100) / 10}k` : `${n}`; }
451
-
452
- function formatTimestamp(value: string | null, locale: string) {
453
- if (!value || !Number.isFinite(Date.parse(value))) return '—';
454
- return new Intl.DateTimeFormat(locale === 'zh' ? 'zh-CN' : 'en', {
455
- month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
456
- }).format(new Date(value));
457
- }
458
-
459
- function readinessChip(state: string) {
460
- const color = state === READINESS_STATES.READY ? '#3fb950'
461
- : state === READINESS_STATES.DEGRADED ? '#c98735'
462
- : state === READINESS_STATES.UNAVAILABLE ? '#f85149' : 'inherit';
463
- return {
464
- fontSize: 10.5, fontWeight: 650, padding: '1px 7px', borderRadius: 99,
465
- border: `1px solid ${state === READINESS_STATES.UNKNOWN ? 'rgba(128,128,128,0.35)' : color}`,
466
- color, opacity: state === READINESS_STATES.UNKNOWN ? 0.58 : 1,
467
- };
468
- }
469
-
470
- function MinimalCrewPanel({ locale, surface, runtime }: { locale: string; surface: string; runtime: any }) {
471
- const zh = locale === 'zh';
472
- const native = surface === CREW_UI_SURFACES.NATIVE;
473
- const title = native ? (zh ? 'DSH Crew Runtime' : 'DSH Crew Runtime') : (zh ? 'DSH Crew Surface 未确认' : 'DSH Crew surface not verified');
474
- const hint = native
475
- ? (zh
476
- ? '这里是隔离的 3210 Crew Harness。请使用 Harness 原生菜单管理 Provider、Harness Models、Agent 预设与底层配置;Crew 编排和宿主集成统一在 3080 管理。'
477
- : '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.')
478
- : (zh
479
- ? '无法用结构化后端证据确认当前界面,因此已保守隐藏 Crew 编排、任务与宿主集成功能。'
480
- : 'Structured backend evidence could not identify this surface, so Crew orchestration, jobs, and host integrations are conservatively hidden.');
481
- const runtimeReady = runtime?.ok === true && runtime?.service === 'dsh-crew-hub';
482
- return (
483
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10, fontSize: 13, lineHeight: 1.55 }}>
484
- <div style={{ ...S.block, background: 'linear-gradient(135deg, rgba(74,158,255,0.08), rgba(128,128,128,0.02))' }}>
485
- <div>
486
- <div style={{ fontSize: 19, fontWeight: 680 }}>{title}</div>
487
- <div style={{ fontSize: 12, opacity: 0.68, marginTop: 3 }}>{hint}</div>
488
- </div>
489
- <div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
490
- <span style={readinessChip(runtimeReady ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
491
- {runtimeReady ? `Runtime ${runtime.runtime_version ?? 'READY'}` : 'Runtime UNKNOWN'}
492
- </span>
493
- <span style={readinessChip(native ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
494
- {native ? '3210 · isolated' : 'surface · unknown'}
495
- </span>
496
- </div>
497
- <a href={CREW_CONTROL_PLANE_URL} target="_blank" rel="noopener noreferrer" style={{
498
- ...S.btn, alignSelf: 'flex-start', textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
499
- borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.08)',
500
- }}>{zh ? '打开 3080 DSH Crew 控制台 →' : 'Open the 3080 DSH Crew console →'}</a>
501
- </div>
502
- </div>
503
- );
504
- }
505
-
506
- function WorkersPanel({ ctx }: { ctx: any }) {
454
+ function ktok(n: number) { return n >= 1000 ? `${Math.round(n / 100) / 10}k` : `${n}`; }
455
+
456
+ function formatTimestamp(value: string | null, locale: string) {
457
+ if (!value || !Number.isFinite(Date.parse(value))) return '—';
458
+ return new Intl.DateTimeFormat(locale === 'zh' ? 'zh-CN' : 'en', {
459
+ month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
460
+ }).format(new Date(value));
461
+ }
462
+
463
+ function readinessChip(state: string) {
464
+ const color = state === READINESS_STATES.READY ? '#3fb950'
465
+ : state === READINESS_STATES.DEGRADED ? '#c98735'
466
+ : state === READINESS_STATES.UNAVAILABLE ? '#f85149' : 'inherit';
467
+ return {
468
+ fontSize: 10.5, fontWeight: 650, padding: '1px 7px', borderRadius: 99,
469
+ border: `1px solid ${state === READINESS_STATES.UNKNOWN ? 'rgba(128,128,128,0.35)' : color}`,
470
+ color, opacity: state === READINESS_STATES.UNKNOWN ? 0.58 : 1,
471
+ };
472
+ }
473
+
474
+ function MinimalCrewPanel({ locale, surface, runtime }: { locale: string; surface: string; runtime: any }) {
475
+ const zh = locale === 'zh';
476
+ const native = surface === CREW_UI_SURFACES.NATIVE;
477
+ const title = native ? (zh ? 'DSH Crew Runtime' : 'DSH Crew Runtime') : (zh ? 'DSH Crew Surface 未确认' : 'DSH Crew surface not verified');
478
+ const hint = native
479
+ ? (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.')
482
+ : (zh
483
+ ? '无法用结构化后端证据确认当前界面,因此已保守隐藏 Crew 编排、任务与宿主集成功能。'
484
+ : 'Structured backend evidence could not identify this surface, so Crew orchestration, jobs, and host integrations are conservatively hidden.');
485
+ const runtimeReady = runtime?.ok === true && runtime?.service === 'dsh-crew-hub';
486
+ return (
487
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10, fontSize: 13, lineHeight: 1.55 }}>
488
+ <div style={{ ...S.block, background: 'linear-gradient(135deg, rgba(74,158,255,0.08), rgba(128,128,128,0.02))' }}>
489
+ <div>
490
+ <div style={{ fontSize: 19, fontWeight: 680 }}>{title}</div>
491
+ <div style={{ fontSize: 12, opacity: 0.68, marginTop: 3 }}>{hint}</div>
492
+ </div>
493
+ <div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
494
+ <span style={readinessChip(runtimeReady ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
495
+ {runtimeReady ? `Runtime ${runtime.runtime_version ?? 'READY'}` : 'Runtime UNKNOWN'}
496
+ </span>
497
+ <span style={readinessChip(native ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
498
+ {native ? '3210 · isolated' : 'surface · unknown'}
499
+ </span>
500
+ </div>
501
+ <a href={CREW_CONTROL_PLANE_URL} target="_blank" rel="noopener noreferrer" style={{
502
+ ...S.btn, alignSelf: 'flex-start', textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
503
+ 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>
505
+ </div>
506
+ </div>
507
+ );
508
+ }
509
+
510
+ function WorkersPanel({ ctx }: { ctx: any }) {
507
511
  const locale = useSyncExternalStore(
508
512
  (notify: () => void) => ctx.on('locale/change', notify),
509
513
  () => ctx.locale.getLocale().active,
@@ -525,23 +529,23 @@ function WorkersPanel({ ctx }: { ctx: any }) {
525
529
  vision_command: string; imagegen_command: string;
526
530
  } | null>(null);
527
531
  const [modelAdd, setModelAdd] = useState<string | null>(null);
528
- const [modelCatalog, setModelCatalog] = useState<any>(null);
529
- const [modelCatalogError, setModelCatalogError] = useState('');
530
- const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
531
- const [providerInventory, setProviderInventory] = useState<any>(null);
532
- const [providerHealth, setProviderHealth] = useState<any[]>([]);
533
- const [credentialRefs, setCredentialRefs] = useState<any[]>([]);
534
- const [credentialUnverified, setCredentialUnverified] = useState<any[]>([]);
535
- const [credentialLifecycleBusy, setCredentialLifecycleBusy] = useState<string | null>(null);
536
- const [providerInventoryError, setProviderInventoryError] = useState('');
537
- const [providerLifecycleBusy, setProviderLifecycleBusy] = useState<string | null>(null);
532
+ const [modelCatalog, setModelCatalog] = useState<any>(null);
533
+ const [modelCatalogError, setModelCatalogError] = useState('');
534
+ const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
535
+ const [providerInventory, setProviderInventory] = useState<any>(null);
536
+ const [providerHealth, setProviderHealth] = useState<any[]>([]);
537
+ const [credentialRefs, setCredentialRefs] = useState<any[]>([]);
538
+ const [credentialUnverified, setCredentialUnverified] = useState<any[]>([]);
539
+ const [credentialLifecycleBusy, setCredentialLifecycleBusy] = useState<string | null>(null);
540
+ const [providerInventoryError, setProviderInventoryError] = useState('');
541
+ const [providerLifecycleBusy, setProviderLifecycleBusy] = useState<string | null>(null);
538
542
  const [modelPickerTier, setModelPickerTier] = useState<'flash' | 'pro' | null>(null);
539
543
  const [modelQuery, setModelQuery] = useState('');
540
- const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
541
- const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
542
- const [surface, setSurface] = useState<string>('detecting');
543
- const [runtimeInfo, setRuntimeInfo] = useState<any>(undefined);
544
- const [readinessSnapshot, setReadinessSnapshot] = useState<any>(undefined);
544
+ const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
545
+ const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
546
+ const [surface, setSurface] = useState<string>('detecting');
547
+ const [runtimeInfo, setRuntimeInfo] = useState<any>(undefined);
548
+ const [readinessSnapshot, setReadinessSnapshot] = useState<any>(undefined);
545
549
 
546
550
  const toggleSection = (sectionId: string) => {
547
551
  setExpandedSections((current) => ({ ...current, [sectionId]: !current[sectionId] }));
@@ -552,13 +556,13 @@ function WorkersPanel({ ctx }: { ctx: any }) {
552
556
  writeSectionState(sectionStorage(), expandedSections);
553
557
  }, [expandedSections]);
554
558
 
555
- useEffect(() => {
556
- if (modelCatalogError) setExpandedSections((current) => openSections(current, ['workflow', 'flash', 'pro']));
557
- }, [modelCatalogError]);
558
-
559
- useEffect(() => {
560
- if (providerInventoryError) setExpandedSections((current) => openSections(current, ['harnessProviders']));
561
- }, [providerInventoryError]);
559
+ useEffect(() => {
560
+ if (modelCatalogError) setExpandedSections((current) => openSections(current, ['workflow', 'flash', 'pro']));
561
+ }, [modelCatalogError]);
562
+
563
+ useEffect(() => {
564
+ if (providerInventoryError) setExpandedSections((current) => openSections(current, ['harnessProviders']));
565
+ }, [providerInventoryError]);
562
566
 
563
567
  useEffect(() => {
564
568
  if (testResult && !testResult.busy && testResult.ok === false) {
@@ -591,63 +595,63 @@ function WorkersPanel({ ctx }: { ctx: any }) {
591
595
  * (DSH's setting may be unset), and the hub localizes its own strings from it. */
592
596
  const withLang = useCallback((path: string) => `${API}${path}${path.includes('?') ? '&' : '?'}lang=${locale === 'zh' ? 'zh' : 'en'}`, [locale]);
593
597
  const get = useCallback(async (path: string) => readJson(await fetch(withLang(path), { cache: 'no-store' }), path), [readJson, withLang]);
594
- const post = useCallback(async (path: string, body: any) => readJson(await fetch(withLang(path), {
598
+ const post = useCallback(async (path: string, body: any) => readJson(await fetch(withLang(path), {
595
599
  method: 'POST', headers: { 'content-type': 'application/json' },
596
600
  body: JSON.stringify({ ...body, lang: locale === 'zh' ? 'zh' : 'en' }),
597
- }), path), [readJson, withLang, locale]);
598
-
599
- const detectSurface = useCallback(async () => {
600
- const readOptional = async (path: string) => {
601
- try {
602
- const response = await fetch(`${API}${path}`, { cache: 'no-store' });
603
- if (!response.ok) return null;
604
- const body = await response.json();
605
- return body && typeof body === 'object' ? body : null;
606
- } catch { return null; }
607
- };
608
- const [bridgeStatus, runtime] = await Promise.all([
609
- readOptional('/bridge-status'),
610
- readOptional('/runtime'),
611
- ]);
612
- setRuntimeInfo(runtime);
613
- setSurface(classifyCrewSurface({ bridgeStatus, runtime }));
614
- }, []);
615
-
616
- useEffect(() => { void detectSurface(); }, [detectSurface]);
601
+ }), path), [readJson, withLang, locale]);
602
+
603
+ const detectSurface = useCallback(async () => {
604
+ const readOptional = async (path: string) => {
605
+ try {
606
+ const response = await fetch(`${API}${path}`, { cache: 'no-store' });
607
+ if (!response.ok) return null;
608
+ const body = await response.json();
609
+ return body && typeof body === 'object' ? body : null;
610
+ } catch { return null; }
611
+ };
612
+ const [bridgeStatus, runtime] = await Promise.all([
613
+ readOptional('/bridge-status'),
614
+ readOptional('/runtime'),
615
+ ]);
616
+ setRuntimeInfo(runtime);
617
+ setSurface(classifyCrewSurface({ bridgeStatus, runtime }));
618
+ }, []);
619
+
620
+ useEffect(() => { void detectSurface(); }, [detectSurface]);
617
621
 
618
622
  const refreshAll = useCallback(async () => {
619
623
  try {
620
- const [j, s, c, pr, pi, ph, cr, ext] = await Promise.all([
621
- get('/jobs'), get('/install/status'), get('/config'), get('/presets'),
622
- get('/providers').catch(() => null), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null), get('/extension').catch(() => null),
623
- ]);
624
+ const [j, s, c, pr, pi, ph, cr, ext] = await Promise.all([
625
+ 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),
627
+ ]);
624
628
  if (j.ok) setJobs(j.jobs ?? []);
625
629
  if (s.ok) setStatus(s.status);
626
630
  if (c.ok) setConfig((prev: any) => prev ?? c.config);
627
- if (pr.ok) setPresetOptions([
631
+ if (pr.ok) setPresetOptions([
628
632
  { value: 'default', label: copy.presetDefault(pr.defaultId) },
629
633
  ...(pr.presets ?? []).map((x: any) => ({ value: x.id, label: x.name ?? x.id })),
630
- ]);
631
- if (pi?.ok) setProviderInventory(pi);
632
- if (ph?.ok) setProviderHealth(ph.health ?? []);
633
- if (cr?.ok) { setCredentialRefs(cr.records ?? []); setCredentialUnverified(cr.unverified_purges ?? []); }
634
- if (ext?.ok) setReadinessSnapshot(ext.extension?.readiness_snapshot ?? undefined);
634
+ ]);
635
+ if (pi?.ok) setProviderInventory(pi);
636
+ if (ph?.ok) setProviderHealth(ph.health ?? []);
637
+ if (cr?.ok) { setCredentialRefs(cr.records ?? []); setCredentialUnverified(cr.unverified_purges ?? []); }
638
+ if (ext?.ok) setReadinessSnapshot(ext.extension?.readiness_snapshot ?? undefined);
635
639
  } catch { /* instance restarting */ }
636
640
  }, [get]);
637
641
 
638
- useEffect(() => {
639
- if (surface !== CREW_UI_SURFACES.OFFICIAL) return undefined;
640
- void refreshAll();
641
- const timer = setInterval(() => {
642
- void Promise.all([get('/jobs'), get('/provider-health').catch(() => null)]).then(([j, health]) => {
643
- if (j.ok) setJobs(j.jobs ?? []);
644
- if (health?.ok) setProviderHealth(health.health ?? []);
645
- }).catch(() => {});
646
- }, 3000);
647
- return () => clearInterval(timer);
648
- }, [surface, refreshAll, get]);
642
+ useEffect(() => {
643
+ if (surface !== CREW_UI_SURFACES.OFFICIAL) return undefined;
644
+ void refreshAll();
645
+ const timer = setInterval(() => {
646
+ void Promise.all([get('/jobs'), get('/provider-health').catch(() => null)]).then(([j, health]) => {
647
+ if (j.ok) setJobs(j.jobs ?? []);
648
+ if (health?.ok) setProviderHealth(health.health ?? []);
649
+ }).catch(() => {});
650
+ }, 3000);
651
+ return () => clearInterval(timer);
652
+ }, [surface, refreshAll, get]);
649
653
 
650
- const refreshHarnessModels = useCallback(async () => {
654
+ const refreshHarnessModels = useCallback(async () => {
651
655
  setModelCatalogBusy(true);
652
656
  setModelCatalogError('');
653
657
  try {
@@ -659,53 +663,53 @@ function WorkersPanel({ ctx }: { ctx: any }) {
659
663
  } finally {
660
664
  setModelCatalogBusy(false);
661
665
  }
662
- }, [get, copy]);
663
-
664
- const refreshProviderInventory = useCallback(async () => {
665
- setProviderInventoryError('');
666
- try {
667
- const [result, health, refs] = await Promise.all([get('/providers'), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null)]);
668
- if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
669
- setProviderInventory(result);
670
- if (health?.ok) setProviderHealth(health.health ?? []);
671
- if (refs?.ok) { setCredentialRefs(refs.records ?? []); setCredentialUnverified(refs.unverified_purges ?? []); }
672
- } catch (error: any) {
673
- setProviderInventoryError(error?.message ?? copy.providerNoInventory);
674
- }
675
- }, [get, copy]);
676
-
677
- const purgeCredentialRef = useCallback(async (ref: any) => {
678
- if (ref?.ownership !== 'crew-owned' || ref?.orphan !== true || ref?.purge_capability !== 'eligible') return;
679
- const referenceId = String(ref.reference_id ?? '');
680
- if (!referenceId || !window.confirm(copy.credentialPurgeConfirm)) return;
681
- setCredentialLifecycleBusy(referenceId);
682
- setNotice(copy.working);
683
- try {
684
- const encoded = encodeURIComponent(referenceId);
685
- const planned = await post(`/credential-references/${encoded}/purge-plan`, {});
686
- if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.credentialPurgeUnavailable);
687
- if (!window.confirm(`${copy.credentialPurgeConfirm}\n\n${referenceId}`)) return;
688
- const result = await readJson(await fetch(withLang(`/credential-references/${encoded}`), {
689
- method: 'DELETE', headers: { 'content-type': 'application/json' },
690
- body: JSON.stringify({ plan_id: planned.plan.plan_id, expected_revision: planned.plan.expected_revision, confirm: true, lang: locale === 'zh' ? 'zh' : 'en' }),
691
- }), `/credential-references/${encoded}`);
692
- if (!result.ok || result.state !== 'VERIFIED') throw new Error(result.code ?? copy.credentialPurgeUnavailable);
693
- setNotice(copy.saved);
694
- await refreshProviderInventory();
695
- } catch (error: any) {
696
- setNotice(String(error?.message ?? error));
697
- } finally {
698
- setCredentialLifecycleBusy(null);
699
- }
700
- }, [copy, locale, post, readJson, refreshProviderInventory, withLang]);
666
+ }, [get, copy]);
701
667
 
702
- useEffect(() => {
703
- if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
704
- }, [surface, refreshHarnessModels]);
705
-
706
- useEffect(() => {
707
- if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshProviderInventory();
708
- }, [surface, refreshProviderInventory]);
668
+ const refreshProviderInventory = useCallback(async () => {
669
+ setProviderInventoryError('');
670
+ try {
671
+ const [result, health, refs] = await Promise.all([get('/providers'), get('/provider-health').catch(() => null), get('/credential-references').catch(() => null)]);
672
+ if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
673
+ setProviderInventory(result);
674
+ if (health?.ok) setProviderHealth(health.health ?? []);
675
+ if (refs?.ok) { setCredentialRefs(refs.records ?? []); setCredentialUnverified(refs.unverified_purges ?? []); }
676
+ } catch (error: any) {
677
+ setProviderInventoryError(error?.message ?? copy.providerNoInventory);
678
+ }
679
+ }, [get, copy]);
680
+
681
+ const purgeCredentialRef = useCallback(async (ref: any) => {
682
+ if (ref?.ownership !== 'crew-owned' || ref?.orphan !== true || ref?.purge_capability !== 'eligible') return;
683
+ const referenceId = String(ref.reference_id ?? '');
684
+ if (!referenceId || !window.confirm(copy.credentialPurgeConfirm)) return;
685
+ setCredentialLifecycleBusy(referenceId);
686
+ setNotice(copy.working);
687
+ try {
688
+ const encoded = encodeURIComponent(referenceId);
689
+ const planned = await post(`/credential-references/${encoded}/purge-plan`, {});
690
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.credentialPurgeUnavailable);
691
+ if (!window.confirm(`${copy.credentialPurgeConfirm}\n\n${referenceId}`)) return;
692
+ const result = await readJson(await fetch(withLang(`/credential-references/${encoded}`), {
693
+ method: 'DELETE', headers: { 'content-type': 'application/json' },
694
+ body: JSON.stringify({ plan_id: planned.plan.plan_id, expected_revision: planned.plan.expected_revision, confirm: true, lang: locale === 'zh' ? 'zh' : 'en' }),
695
+ }), `/credential-references/${encoded}`);
696
+ if (!result.ok || result.state !== 'VERIFIED') throw new Error(result.code ?? copy.credentialPurgeUnavailable);
697
+ setNotice(copy.saved);
698
+ await refreshProviderInventory();
699
+ } catch (error: any) {
700
+ setNotice(String(error?.message ?? error));
701
+ } finally {
702
+ setCredentialLifecycleBusy(null);
703
+ }
704
+ }, [copy, locale, post, readJson, refreshProviderInventory, withLang]);
705
+
706
+ useEffect(() => {
707
+ if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
708
+ }, [surface, refreshHarnessModels]);
709
+
710
+ useEffect(() => {
711
+ if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshProviderInventory();
712
+ }, [surface, refreshProviderInventory]);
709
713
 
710
714
  const act = useCallback(async (target: string, confirmName?: string) => {
711
715
  if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
@@ -718,47 +722,47 @@ function WorkersPanel({ ctx }: { ctx: any }) {
718
722
  } catch (e: any) { setNotice(String(e?.message ?? e)); } finally { setBusy(false); }
719
723
  }, [post, get, copy]);
720
724
 
721
- const applyPatch = useCallback(async (patch: Record<string, any>) => {
722
- try {
723
- const result = await post('/config', patch);
724
- if (!result.ok) throw new Error(result.error ?? 'Configuration save failed');
725
- setConfig(result.config);
726
- setNotice(copy.saved);
727
- setTimeout(() => setNotice(''), 1500);
728
- return true;
729
- } catch (error: any) {
730
- setNotice(String(error?.message ?? error));
731
- try {
732
- const authoritative = await get('/config');
733
- if (authoritative.ok) setConfig(authoritative.config);
734
- } catch { /* keep the error visible while the Hub is unavailable */ }
735
- return false;
736
- }
737
- }, [post, get, copy]);
725
+ const applyPatch = useCallback(async (patch: Record<string, any>) => {
726
+ try {
727
+ const result = await post('/config', patch);
728
+ if (!result.ok) throw new Error(result.error ?? 'Configuration save failed');
729
+ setConfig(result.config);
730
+ setNotice(copy.saved);
731
+ setTimeout(() => setNotice(''), 1500);
732
+ return true;
733
+ } catch (error: any) {
734
+ setNotice(String(error?.message ?? error));
735
+ try {
736
+ const authoritative = await get('/config');
737
+ if (authoritative.ok) setConfig(authoritative.config);
738
+ } catch { /* keep the error visible while the Hub is unavailable */ }
739
+ return false;
740
+ }
741
+ }, [post, get, copy]);
738
742
  /** Selects & checkboxes: apply immediately. */
739
743
  const field = (key: string, value: any) => {
740
744
  setConfig((c: any) => ({ ...c, [key]: value }));
741
- void applyPatch({ [key]: value });
745
+ void applyPatch({ [key]: value });
746
+ };
747
+ /** Text/number inputs: track locally, apply on blur. */
748
+ const fieldLocal = (key: string, value: any) => setConfig((c: any) => ({ ...c, [key]: value }));
749
+ const rolePolicyField = (role: 'worker' | 'review', patch: Record<string, any>) => {
750
+ const effectivePatch = role === 'worker' && (patch.ordering === 'manual' || patch.ordering === 'health-aware')
751
+ ? { ...patch, adaptive: { ...normalizeAdaptive(config?.worker?.model_policy?.adaptive), enabled: patch.ordering === 'health-aware' } }
752
+ : patch;
753
+ setConfig((current: any) => ({
754
+ ...current,
755
+ [role]: {
756
+ ...current?.[role],
757
+ model_policy: { ...current?.[role]?.model_policy, ...effectivePatch },
758
+ },
759
+ }));
760
+ void applyPatch({ [role]: { model_policy: effectivePatch } });
761
+ };
762
+ const reviewField = (patch: Record<string, any>) => {
763
+ setConfig((current: any) => ({ ...current, review: { ...current?.review, ...patch } }));
764
+ void applyPatch({ review: patch });
742
765
  };
743
- /** Text/number inputs: track locally, apply on blur. */
744
- const fieldLocal = (key: string, value: any) => setConfig((c: any) => ({ ...c, [key]: value }));
745
- const rolePolicyField = (role: 'worker' | 'review', patch: Record<string, any>) => {
746
- const effectivePatch = role === 'worker' && (patch.ordering === 'manual' || patch.ordering === 'health-aware')
747
- ? { ...patch, adaptive: { ...normalizeAdaptive(config?.worker?.model_policy?.adaptive), enabled: patch.ordering === 'health-aware' } }
748
- : patch;
749
- setConfig((current: any) => ({
750
- ...current,
751
- [role]: {
752
- ...current?.[role],
753
- model_policy: { ...current?.[role]?.model_policy, ...effectivePatch },
754
- },
755
- }));
756
- void applyPatch({ [role]: { model_policy: effectivePatch } });
757
- };
758
- const reviewField = (patch: Record<string, any>) => {
759
- setConfig((current: any) => ({ ...current, review: { ...current?.review, ...patch } }));
760
- void applyPatch({ review: patch });
761
- };
762
766
 
763
767
  // --- custom providers & user-added models -------------------------------
764
768
  const customProviders: any[] = config?.custom_providers ?? [];
@@ -795,7 +799,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
795
799
  const cur = extraModels[p] ?? [];
796
800
  const nextExtra = { ...extraModels, [p]: cur.includes(m) ? cur : [...cur, m] };
797
801
  setConfig((c: any) => ({ ...c, extra_models: nextExtra, vision_model: m }));
798
- void applyPatch({ extra_models: nextExtra, vision_model: m });
802
+ void applyPatch({ extra_models: nextExtra, vision_model: m });
799
803
  };
800
804
  const removeCurrentModel = () => {
801
805
  if (!config) return;
@@ -804,7 +808,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
804
808
  const nextExtra = { ...extraModels, [p]: (extraModels[p] ?? []).filter((x) => x !== m) };
805
809
  const fallback = visionModelOptions.find((o) => o.value !== m)?.value ?? 'default';
806
810
  setConfig((c: any) => ({ ...c, extra_models: nextExtra, vision_model: fallback }));
807
- void applyPatch({ extra_models: nextExtra, vision_model: fallback });
811
+ void applyPatch({ extra_models: nextExtra, vision_model: fallback });
808
812
  };
809
813
  const saveProviderForm = () => {
810
814
  if (!provForm || !config) return;
@@ -866,93 +870,179 @@ function WorkersPanel({ ctx }: { ctx: any }) {
866
870
  </div>
867
871
  );
868
872
  };
869
- const deleteProvider = (id: string) => {
873
+ const deleteProvider = (id: string) => {
870
874
  if (!config) return;
871
875
  const patch: any = { custom_providers: customProviders.filter((p) => p.id !== id) };
872
876
  if (config.vision_provider === id) { patch.vision_provider = 'claude-code'; patch.vision_model = 'haiku'; }
873
877
  if (config.imagegen_provider === id) patch.imagegen_provider = 'codex';
874
878
  setConfig((c: any) => ({ ...c, ...patch }));
875
- void applyPatch(patch);
876
- };
877
-
878
- const deleteHarnessProvider = async (record: any) => {
879
- if (!record || record.ownership === 'harness' || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0) {
880
- setNotice(copy.providerDeleteBlocked);
881
- return;
882
- }
883
- let replacementDefault: string | undefined;
884
- if (record.references?.harness_default === true) {
885
- const candidate = (providerInventory?.records ?? []).find((item: any) => item.id !== record.id
886
- && item.desired_state !== 'absent' && item.ownership !== 'harness' && (item.models?.length ?? 0) > 0)?.id ?? '';
887
- const entered = window.prompt(copy.providerReplacement, candidate);
888
- if (!entered?.trim()) return;
889
- replacementDefault = entered.trim();
890
- }
891
- setProviderLifecycleBusy(record.id);
892
- setNotice(copy.working);
893
- try {
894
- const encoded = encodeURIComponent(record.id);
895
- const planned = await post(`/providers/${encoded}/delete-plan`, replacementDefault ? { replacement_default: replacementDefault } : {});
896
- if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
897
- if (!window.confirm(`${copy.providerPlanReady}\n\n${copy.providerDeleteConfirm}\n${(planned.plan.will_remove ?? []).join('\n')}`)) return;
898
- const path = `/providers/${encoded}`;
899
- const result = await readJson(await fetch(withLang(path), {
900
- method: 'DELETE', headers: { 'content-type': 'application/json' },
901
- body: JSON.stringify({ plan_id: planned.plan.plan_id, expected_revision: planned.plan.expected_revision, confirm: true, lang: locale === 'zh' ? 'zh' : 'en' }),
902
- }), path);
903
- if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
904
- if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
905
- const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
906
- const restarted = await readJson(await fetch(restartPath, {
907
- method: 'POST', headers: { 'content-type': 'application/json' },
908
- body: JSON.stringify({ confirm: true }),
909
- }), restartPath);
910
- if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
911
- const verified = await post(`/providers/${encoded}/verify-delete`, { transaction_id: planned.plan.plan_id, confirm: true });
912
- if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
913
- }
914
- setNotice(copy.saved);
915
- await refreshProviderInventory();
916
- } catch (error: any) {
917
- setNotice(String(error?.message ?? error));
918
- } finally {
919
- setProviderLifecycleBusy(null);
920
- }
921
- };
922
-
923
- const rollbackHarnessProvider = async (record: any, transactionId: string) => {
924
- if (!record || !transactionId || record.desired_state !== 'absent') {
925
- setNotice(copy.providerRollbackUnavailable);
926
- return;
927
- }
928
- if (!window.confirm(copy.providerRollbackConfirm)) return;
929
- setProviderLifecycleBusy(`rollback:${record.id}`);
930
- setNotice(copy.working);
931
- try {
932
- const encoded = encodeURIComponent(record.id);
933
- const result = await post(`/providers/${encoded}/rollback`, { transaction_id: transactionId, confirm: true });
934
- if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
935
- if (result.restart_required === true && result.state === 'ROLLBACK_PENDING') {
936
- const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
937
- const restarted = await readJson(await fetch(restartPath, {
938
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
939
- }), restartPath);
940
- if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
941
- const verified = await post(`/providers/${encoded}/verify-rollback`, { transaction_id: transactionId, confirm: true });
942
- if (!verified.ok || verified.state !== 'ROLLED_BACK') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
943
- } else if (result.state !== 'ROLLED_BACK') {
944
- throw new Error(result.code ?? copy.providerLifecycleError);
945
- }
946
- setNotice(copy.saved);
947
- await refreshProviderInventory();
948
- } catch (error: any) {
949
- setNotice(String(error?.message ?? error));
950
- } finally {
951
- setProviderLifecycleBusy(null);
952
- }
953
- };
954
-
955
- /** Card: title + one-line description header, then its fields. */
879
+ void applyPatch(patch);
880
+ };
881
+
882
+ const deleteHarnessProvider = async (record: any) => {
883
+ if (!record || record.delete_capability !== 'supported' || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0) {
884
+ setNotice(copy.providerDeleteBlocked);
885
+ return;
886
+ }
887
+ let replacementDefault: string | undefined;
888
+ if (record.references?.harness_default === true) {
889
+ const candidate = (providerInventory?.records ?? []).find((item: any) => item.id === 'deepseek-official'
890
+ && item.id !== record.id && item.desired_state !== 'absent' && (item.models?.length ?? 0) > 0)?.id
891
+ ?? (providerInventory?.records ?? []).find((item: any) => item.id !== record.id
892
+ && item.desired_state !== 'absent' && item.delete_capability !== 'source-unresolved' && (item.models?.length ?? 0) > 0)?.id ?? '';
893
+ const entered = window.prompt(copy.providerReplacement, candidate);
894
+ if (!entered?.trim()) return;
895
+ replacementDefault = entered.trim();
896
+ }
897
+ setProviderLifecycleBusy(record.id);
898
+ setNotice(copy.working);
899
+ try {
900
+ const encoded = encodeURIComponent(record.id);
901
+ const planned = await post(`/providers/${encoded}/delete-plan`, replacementDefault ? { replacement_default: replacementDefault } : {});
902
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
903
+ if (!window.confirm(`${copy.providerPlanReady}\n\n${copy.providerDeleteConfirm}\n${(planned.plan.will_remove ?? []).join('\n')}`)) return;
904
+ const path = `/providers/${encoded}`;
905
+ const result = await readJson(await fetch(withLang(path), {
906
+ method: 'DELETE', headers: { 'content-type': 'application/json' },
907
+ body: JSON.stringify({ plan_id: planned.plan.plan_id, expected_revision: planned.plan.expected_revision, confirm: true, lang: locale === 'zh' ? 'zh' : 'en' }),
908
+ }), path);
909
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
910
+ if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
911
+ const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
912
+ const restarted = await readJson(await fetch(restartPath, {
913
+ method: 'POST', headers: { 'content-type': 'application/json' },
914
+ body: JSON.stringify({ confirm: true }),
915
+ }), restartPath);
916
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
917
+ const verified = await post(`/providers/${encoded}/verify-delete`, { transaction_id: planned.plan.plan_id, confirm: true });
918
+ if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
919
+ }
920
+ setNotice(copy.saved);
921
+ await refreshProviderInventory();
922
+ } catch (error: any) {
923
+ try { await refreshProviderInventory(); } catch { /* preserve the original lifecycle error */ }
924
+ setNotice(`${String(error?.message ?? error)} · ${copy.providerDeletePending}`);
925
+ } finally {
926
+ setProviderLifecycleBusy(null);
927
+ }
928
+ };
929
+
930
+ const migrateHarnessProvider = async (entry: any) => {
931
+ if (!entry || !['promote-existing-user', 'materialize-user'].includes(entry.action)) {
932
+ setNotice(entry?.blocked_reason ?? copy.providerLayerMigrationHint);
933
+ return;
934
+ }
935
+ if (!window.confirm(copy.providerLayerMigrationConfirm)) return;
936
+ const id = String(entry.provider_id ?? '');
937
+ if (!id) return;
938
+ setProviderLifecycleBusy(`migrate:${id}`);
939
+ setNotice(copy.providerLayerMigrating);
940
+ try {
941
+ const encoded = encodeURIComponent(id);
942
+ const planned = await post(`/providers/${encoded}/migrate-plan`, {});
943
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
944
+ if (!window.confirm(`${copy.providerLayerMigrationConfirm}\n\n${id}`)) return;
945
+ const result = await post(`/providers/${encoded}/migrate`, { plan_id: planned.plan.plan_id, confirm: true });
946
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
947
+ if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
948
+ const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
949
+ const restarted = await readJson(await fetch(restartPath, {
950
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
951
+ }), restartPath);
952
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
953
+ const verified = await post(`/providers/${encoded}/verify-migration`, { transaction_id: planned.plan.plan_id, confirm: true });
954
+ if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
955
+ }
956
+ setNotice(copy.saved);
957
+ await refreshProviderInventory();
958
+ } catch (error: any) {
959
+ try { await refreshProviderInventory(); } catch { /* preserve the original migration error */ }
960
+ setNotice(String(error?.message ?? error));
961
+ } finally {
962
+ setProviderLifecycleBusy(null);
963
+ }
964
+ };
965
+
966
+ const rollbackHarnessMigration = async (record: any, transactionId: string) => {
967
+ if (!record?.id || !transactionId || !window.confirm(copy.providerRollbackConfirm)) return;
968
+ setProviderLifecycleBusy(`rollback-migrate:${record.id}`);
969
+ setNotice(copy.working);
970
+ try {
971
+ const encoded = encodeURIComponent(record.id);
972
+ const result = await post(`/providers/${encoded}/rollback-migration`, { transaction_id: transactionId, confirm: true });
973
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
974
+ if (result.restart_required === true) {
975
+ const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
976
+ const restarted = await readJson(await fetch(restartPath, {
977
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
978
+ }), restartPath);
979
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
980
+ }
981
+ setNotice(copy.saved);
982
+ await refreshProviderInventory();
983
+ } catch (error: any) {
984
+ setNotice(String(error?.message ?? error));
985
+ try { await refreshProviderInventory(); } catch { /* preserve the original rollback error */ }
986
+ } finally { setProviderLifecycleBusy(null); }
987
+ };
988
+
989
+ const rollbackHarnessProvider = async (record: any, transactionId: string, transactionState: string = 'VERIFIED') => {
990
+ const continuing = ['ROLLBACK_PENDING', 'ROLLBACK_RESTART_PENDING'].includes(transactionState);
991
+ const recoverable = ['PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_APPLYING', 'ROLLBACK_RESTORED'].includes(transactionState);
992
+ if (!record || !transactionId || (!continuing && !recoverable && record.desired_state !== 'absent')) {
993
+ setNotice(copy.providerRollbackUnavailable);
994
+ return;
995
+ }
996
+ if (!window.confirm(copy.providerRollbackConfirm)) return;
997
+ setProviderLifecycleBusy(`rollback:${record.id}`);
998
+ setNotice(copy.working);
999
+ try {
1000
+ const encoded = encodeURIComponent(record.id);
1001
+ const result = continuing
1002
+ ? { ok: true, restart_required: true, state: 'ROLLBACK_PENDING' }
1003
+ : await post(`/providers/${encoded}/rollback`, { transaction_id: transactionId, confirm: true });
1004
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
1005
+ if (result.restart_required === true && result.state === 'ROLLBACK_PENDING') {
1006
+ const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
1007
+ const restarted = await readJson(await fetch(restartPath, {
1008
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
1009
+ }), restartPath);
1010
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
1011
+ const verified = await post(`/providers/${encoded}/verify-rollback`, { transaction_id: transactionId, confirm: true });
1012
+ if (!verified.ok || verified.state !== 'ROLLED_BACK') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
1013
+ } else if (result.state !== 'ROLLED_BACK') {
1014
+ throw new Error(result.code ?? copy.providerLifecycleError);
1015
+ }
1016
+ setNotice(copy.saved);
1017
+ await refreshProviderInventory();
1018
+ } catch (error: any) {
1019
+ try { await refreshProviderInventory(); } catch { /* preserve the original lifecycle error */ }
1020
+ setNotice(`${String(error?.message ?? error)} · ${copy.providerDeletePending}`);
1021
+ } finally {
1022
+ setProviderLifecycleBusy(null);
1023
+ }
1024
+ };
1025
+
1026
+ const quarantineRecoveryTransaction = async (entry: any) => {
1027
+ const actionId = typeof entry?.action_id === 'string' ? entry.action_id : '';
1028
+ if (!actionId || entry?.recoverable === true) return;
1029
+ if (!window.confirm(copy.recoveryQuarantineConfirm)) return;
1030
+ setProviderLifecycleBusy(`quarantine:${actionId}`);
1031
+ setNotice(copy.working);
1032
+ try {
1033
+ const result = await post('/providers/_recovery/quarantine', { action_id: actionId, confirm: true });
1034
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
1035
+ setNotice(copy.saved);
1036
+ await refreshProviderInventory();
1037
+ } catch (error: any) {
1038
+ try { await refreshProviderInventory(); } catch { /* preserve the original recovery error */ }
1039
+ setNotice(String(error?.message ?? error));
1040
+ } finally {
1041
+ setProviderLifecycleBusy(null);
1042
+ }
1043
+ };
1044
+
1045
+ /** Card: title + one-line description header, then its fields. */
956
1046
  const block = (text: { t: string; d: string }, fields: any, gridded = true) => (
957
1047
  <div style={S.block}>
958
1048
  <div>
@@ -963,69 +1053,69 @@ function WorkersPanel({ ctx }: { ctx: any }) {
963
1053
  </div>
964
1054
  );
965
1055
 
966
- const integrationRow = (name: string, installed: boolean, ready: boolean, extra: any, installTarget: string, uninstallTarget: string, tips: any) => (
967
- <div style={S.card}>
968
- <span style={{ fontWeight: 600, fontSize: 13, minWidth: 92 }}>{name}</span>
969
- <span style={S.chip(installed)}>{installed ? `● ${copy.installed}` : `○ ${copy.notInstalled}`}</span>
970
- <span style={S.chip(ready)}>{ready ? `● ${copy.ready}` : `○ ${copy.notReady}`}</span>
971
- {extra}
1056
+ const integrationRow = (name: string, installed: boolean, ready: boolean, extra: any, installTarget: string, uninstallTarget: string, tips: any) => (
1057
+ <div style={S.card}>
1058
+ <span style={{ fontWeight: 600, fontSize: 13, minWidth: 92 }}>{name}</span>
1059
+ <span style={S.chip(installed)}>{installed ? `● ${copy.installed}` : `○ ${copy.notInstalled}`}</span>
1060
+ <span style={S.chip(ready)}>{ready ? `● ${copy.ready}` : `○ ${copy.notReady}`}</span>
1061
+ {extra}
972
1062
  <span style={{ flex: 1 }} />
973
1063
  {!installed && <button style={S.btn} title={tips.install} disabled={busy} onClick={() => { void act(installTarget); }}>{copy.install}</button>}
974
1064
  {installed && <button style={S.btn} title={tips.update} disabled={busy} onClick={() => { void act(installTarget); }}>{copy.update}</button>}
975
1065
  {installed && <button style={{ ...S.btn, opacity: 0.75 }} title={tips.restore} disabled={busy} onClick={() => { void act(uninstallTarget, name); }}>{copy.restore}</button>}
976
1066
  </div>
977
- );
978
-
979
- const adaptive = normalizeAdaptive(config?.worker?.model_policy?.adaptive);
980
- const setAdaptiveLocal = (candidate: AdaptiveConfig) => {
981
- const next = normalizeAdaptive(candidate);
982
- const ordering = next.enabled ? 'health-aware' : 'manual';
983
- setConfig((current: any) => ({
984
- ...current,
985
- worker: {
986
- ...current.worker,
987
- model_policy: { ...current.worker?.model_policy, adaptive: next, ordering },
988
- },
989
- }));
990
- return { next, ordering };
991
- };
992
- const saveAdaptive = (candidate: AdaptiveConfig) => {
993
- const { next, ordering } = setAdaptiveLocal(candidate);
994
- void applyPatch({ worker: { model_policy: { adaptive: next, ordering } } });
995
- };
996
- const modelActivity = aggregateModelInvocations(jobs);
997
- const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
998
- const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface, readinessSnapshot });
999
-
1000
- if (!currentSurfaceResponsibilities.fullControlPlane) {
1001
- return <MinimalCrewPanel locale={locale} surface={surface} runtime={runtimeInfo} />;
1002
- }
1003
-
1004
- return (
1005
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 13, lineHeight: 1.55 }}>
1006
- <div style={{
1007
- border: '1px solid rgba(128,128,128,0.24)', borderRadius: 12, padding: '14px 15px',
1008
- display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap',
1009
- background: 'linear-gradient(135deg, rgba(74,158,255,0.10), rgba(128,128,128,0.025) 56%)',
1010
- }}>
1011
- <div style={{ minWidth: 240, flex: 1 }}>
1012
- <div style={{ fontSize: 20, fontWeight: 680, letterSpacing: '-0.01em' }}>{copy.title}</div>
1013
- <div style={{ opacity: 0.7, fontSize: 12.5, marginTop: 2 }}>{copy.intro}</div>
1014
- <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
1015
- <span style={S.chip(!!status?.codex?.ready)}>Codex {status?.codex?.ready ? 'READY' : 'CHECK'}</span>
1016
- <span style={S.chip(!!status?.claude?.ready)}>Claude {status?.claude?.ready ? 'READY' : 'CHECK'}</span>
1017
- <span style={S.chip(!!status?.zcode?.ready)}>ZCode {status?.zcode?.ready ? 'READY' : 'CHECK'}</span>
1018
- <span style={S.chip(jobs.some((job) => job.status === 'running'))}>{jobs.filter((job) => job.status === 'running').length} running</span>
1019
- </div>
1020
- </div>
1021
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 3 }}>
1022
- <a href={CREW_HARNESS_URL} target="_blank" rel="noopener noreferrer" style={{
1023
- ...S.btn, textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
1024
- borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.10)',
1025
- }}>{copy.openHarness}</a>
1026
- <span style={{ fontSize: 10.5, opacity: 0.52 }}>{copy.harnessHint}</span>
1027
- </div>
1028
- </div>
1067
+ );
1068
+
1069
+ const adaptive = normalizeAdaptive(config?.worker?.model_policy?.adaptive);
1070
+ const setAdaptiveLocal = (candidate: AdaptiveConfig) => {
1071
+ const next = normalizeAdaptive(candidate);
1072
+ const ordering = next.enabled ? 'health-aware' : 'manual';
1073
+ setConfig((current: any) => ({
1074
+ ...current,
1075
+ worker: {
1076
+ ...current.worker,
1077
+ model_policy: { ...current.worker?.model_policy, adaptive: next, ordering },
1078
+ },
1079
+ }));
1080
+ return { next, ordering };
1081
+ };
1082
+ const saveAdaptive = (candidate: AdaptiveConfig) => {
1083
+ const { next, ordering } = setAdaptiveLocal(candidate);
1084
+ void applyPatch({ worker: { model_policy: { adaptive: next, ordering } } });
1085
+ };
1086
+ const modelActivity = aggregateModelInvocations(jobs);
1087
+ const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
1088
+ const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface, readinessSnapshot });
1089
+
1090
+ if (!currentSurfaceResponsibilities.fullControlPlane) {
1091
+ return <MinimalCrewPanel locale={locale} surface={surface} runtime={runtimeInfo} />;
1092
+ }
1093
+
1094
+ return (
1095
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 13, lineHeight: 1.55 }}>
1096
+ <div style={{
1097
+ border: '1px solid rgba(128,128,128,0.24)', borderRadius: 12, padding: '14px 15px',
1098
+ display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap',
1099
+ background: 'linear-gradient(135deg, rgba(74,158,255,0.10), rgba(128,128,128,0.025) 56%)',
1100
+ }}>
1101
+ <div style={{ minWidth: 240, flex: 1 }}>
1102
+ <div style={{ fontSize: 20, fontWeight: 680, letterSpacing: '-0.01em' }}>{copy.title}</div>
1103
+ <div style={{ opacity: 0.7, fontSize: 12.5, marginTop: 2 }}>{copy.intro}</div>
1104
+ <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
1105
+ <span style={S.chip(!!status?.codex?.ready)}>Codex {status?.codex?.ready ? 'READY' : 'CHECK'}</span>
1106
+ <span style={S.chip(!!status?.claude?.ready)}>Claude {status?.claude?.ready ? 'READY' : 'CHECK'}</span>
1107
+ <span style={S.chip(!!status?.zcode?.ready)}>ZCode {status?.zcode?.ready ? 'READY' : 'CHECK'}</span>
1108
+ <span style={S.chip(jobs.some((job) => job.status === 'running'))}>{jobs.filter((job) => job.status === 'running').length} running</span>
1109
+ </div>
1110
+ </div>
1111
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 3 }}>
1112
+ <a href={CREW_HARNESS_URL} target="_blank" rel="noopener noreferrer" style={{
1113
+ ...S.btn, textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
1114
+ borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.10)',
1115
+ }}>{copy.openHarness}</a>
1116
+ <span style={{ fontSize: 10.5, opacity: 0.52 }}>{copy.harnessHint}</span>
1117
+ </div>
1118
+ </div>
1029
1119
 
1030
1120
  <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 5 }}>
1031
1121
  <div style={{ ...S.section, margin: 0, flex: 1 }}>{copy.globalConfig}</div>
@@ -1033,36 +1123,36 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1033
1123
  <button type="button" style={{ ...S.btn, padding: '3px 9px' }} onClick={() => toggleAllSections(false)}>{copy.collapseAll}</button>
1034
1124
  </div>
1035
1125
 
1036
- <CollapsibleSection sectionId="integrations" title={copy.sectionNames.integrations}
1037
- summary={sectionSummary(status?.claude?.ready ? 'Claude READY' : 'Claude CHECK', `${status?.codex?.ready ? 'Codex READY' : 'Codex CHECK'} · ${status?.zcode?.ready ? 'ZCode READY' : 'ZCode CHECK'}`)}
1038
- expanded={!!expandedSections.integrations} onToggle={() => toggleSection('integrations')}>
1039
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1040
- <div style={S.block}>
1041
- <div>
1042
- <div style={S.settingTitle}>{copy.hostReadiness}</div>
1043
- <div style={S.settingDesc}>{copy.hostReadinessHint}</div>
1044
- </div>
1045
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px 10px' }}>
1046
- {hostReadiness.map((row) => (
1047
- <div key={row.id} style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 0, padding: '4px 7px', border: '1px solid rgba(128,128,128,0.16)', borderRadius: 7 }}>
1048
- <span style={{ flex: 1, minWidth: 0, fontSize: 11.5 }}>{(copy as any).readinessLabels[row.id]}</span>
1049
- {row.detail && <span style={{ ...S.mono, opacity: 0.55 }}>{row.detail}</span>}
1050
- <span style={readinessChip(row.state)}>{(copy as any).readinessStates[row.state]}</span>
1051
- </div>
1052
- ))}
1053
- </div>
1054
- </div>
1055
- {integrationRow('Claude Code', !!status?.claude?.installed, !!status?.claude?.ready,
1056
- status?.claude?.installed ? <span style={S.chip(!!status?.claude?.hud)}>{copy.hud}</span> : null,
1057
- 'claude', 'claude-uninstall', (copy as any).tips.claude)}
1058
- {integrationRow('Codex', !!status?.codex?.installed, !!status?.codex?.ready,
1059
- status?.codex?.ready ? null : <span title={(status?.codex?.missing ?? []).join(', ')} style={{ fontSize: 11, opacity: 0.6 }}>{(status?.codex?.missing ?? []).join(' · ')}</span>,
1060
- 'codex', 'codex-uninstall', (copy as any).tips.codex)}
1061
- {integrationRow('ZCode', !!status?.zcode?.installed, !!status?.zcode?.ready,
1062
- status?.zcode?.ready ? null : <span title={(status?.zcode?.missing ?? []).join(', ')} style={{ fontSize: 11, opacity: 0.6 }}>{(status?.zcode?.missing ?? []).join(' · ')}</span>,
1063
- 'zcode', 'zcode-uninstall', (copy as any).tips.zcode)}
1064
- </div>
1065
- </CollapsibleSection>
1126
+ <CollapsibleSection sectionId="integrations" title={copy.sectionNames.integrations}
1127
+ summary={sectionSummary(status?.claude?.ready ? 'Claude READY' : 'Claude CHECK', `${status?.codex?.ready ? 'Codex READY' : 'Codex CHECK'} · ${status?.zcode?.ready ? 'ZCode READY' : 'ZCode CHECK'}`)}
1128
+ expanded={!!expandedSections.integrations} onToggle={() => toggleSection('integrations')}>
1129
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1130
+ <div style={S.block}>
1131
+ <div>
1132
+ <div style={S.settingTitle}>{copy.hostReadiness}</div>
1133
+ <div style={S.settingDesc}>{copy.hostReadinessHint}</div>
1134
+ </div>
1135
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px 10px' }}>
1136
+ {hostReadiness.map((row) => (
1137
+ <div key={row.id} style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 0, padding: '4px 7px', border: '1px solid rgba(128,128,128,0.16)', borderRadius: 7 }}>
1138
+ <span style={{ flex: 1, minWidth: 0, fontSize: 11.5 }}>{(copy as any).readinessLabels[row.id]}</span>
1139
+ {row.detail && <span style={{ ...S.mono, opacity: 0.55 }}>{row.detail}</span>}
1140
+ <span style={readinessChip(row.state)}>{(copy as any).readinessStates[row.state]}</span>
1141
+ </div>
1142
+ ))}
1143
+ </div>
1144
+ </div>
1145
+ {integrationRow('Claude Code', !!status?.claude?.installed, !!status?.claude?.ready,
1146
+ status?.claude?.installed ? <span style={S.chip(!!status?.claude?.hud)}>{copy.hud}</span> : null,
1147
+ 'claude', 'claude-uninstall', (copy as any).tips.claude)}
1148
+ {integrationRow('Codex', !!status?.codex?.installed, !!status?.codex?.ready,
1149
+ status?.codex?.ready ? null : <span title={(status?.codex?.missing ?? []).join(', ')} style={{ fontSize: 11, opacity: 0.6 }}>{(status?.codex?.missing ?? []).join(' · ')}</span>,
1150
+ 'codex', 'codex-uninstall', (copy as any).tips.codex)}
1151
+ {integrationRow('ZCode', !!status?.zcode?.installed, !!status?.zcode?.ready,
1152
+ status?.zcode?.ready ? null : <span title={(status?.zcode?.missing ?? []).join(', ')} style={{ fontSize: 11, opacity: 0.6 }}>{(status?.zcode?.missing ?? []).join(' · ')}</span>,
1153
+ 'zcode', 'zcode-uninstall', (copy as any).tips.zcode)}
1154
+ </div>
1155
+ </CollapsibleSection>
1066
1156
 
1067
1157
  {config && (<>
1068
1158
  {(() => {
@@ -1088,18 +1178,18 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1088
1178
  const savePriority = (tier: string, list: any[]) => {
1089
1179
  const key = `${tier}_model_priority`;
1090
1180
  setConfig((current: any) => ({ ...current, [key]: list, [`${tier}_model_priority_configured`]: true }));
1091
- void applyPatch({ [key]: list });
1181
+ void applyPatch({ [key]: list });
1092
1182
  };
1093
- const modelMeta = (ref: any) => {
1183
+ const modelMeta = (ref: any) => {
1094
1184
  const provider = catalogProviders.find((item) => item.id === ref.provider);
1095
1185
  const model = provider?.models?.find((item: any) => item.id === ref.model);
1096
- return { provider, model };
1097
- };
1098
- const modelHealth = (ref: any) => providerHealth.find((entry: any) => entry.provider === ref.provider && entry.model === ref.model && entry.fresh === true) ?? null;
1099
- const healthLabel = (health: any) => health?.state ? String(health.state).toUpperCase().replace(/-/g, ' ') : copy.healthUnprobed;
1100
- const healthAge = (health: any) => Number.isFinite(health?.observed_at)
1101
- ? copy.checkedAgo(Math.max(0, Math.round((Date.now() - health.observed_at) / 1000)))
1102
- : '';
1186
+ return { provider, model };
1187
+ };
1188
+ const modelHealth = (ref: any) => providerHealth.find((entry: any) => entry.provider === ref.provider && entry.model === ref.model && entry.fresh === true) ?? null;
1189
+ const healthLabel = (health: any) => health?.state ? String(health.state).toUpperCase().replace(/-/g, ' ') : copy.healthUnprobed;
1190
+ const healthAge = (health: any) => Number.isFinite(health?.observed_at)
1191
+ ? copy.checkedAgo(Math.max(0, Math.round((Date.now() - health.observed_at) / 1000)))
1192
+ : '';
1103
1193
  const filteredProviders = catalogProviders.map((provider) => ({
1104
1194
  ...provider,
1105
1195
  models: (provider.models ?? []).filter((model: any) => {
@@ -1121,19 +1211,19 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1121
1211
  {priority.length === 0 && <div style={{ fontSize: 12, opacity: 0.55, marginBottom: 6 }}>{copy.noPriority}</div>}
1122
1212
  {ambiguousPreferred && <div style={{ fontSize: 11.5, color: '#c98735', marginBottom: 6 }}>{copy.needsModelSelection}</div>}
1123
1213
  <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
1124
- {priority.map((ref, index) => {
1125
- const meta = modelMeta(ref);
1126
- const health = modelHealth(ref);
1127
- const warning = !meta.provider ? copy.providerUnavailable : !meta.model ? copy.notAdvertised : '';
1214
+ {priority.map((ref, index) => {
1215
+ const meta = modelMeta(ref);
1216
+ const health = modelHealth(ref);
1217
+ const warning = !meta.provider ? copy.providerUnavailable : !meta.model ? copy.notAdvertised : '';
1128
1218
  return (
1129
1219
  <div key={refKey(ref)} style={{ display: 'flex', gap: 7, alignItems: 'center', padding: '6px 8px', border: '1px solid rgba(128,128,128,0.2)', borderRadius: 6 }}>
1130
1220
  <span style={{ opacity: 0.55, width: 18 }}>{index + 1}.</span>
1131
1221
  <span style={{ flex: 1, minWidth: 0 }}>
1132
1222
  <span style={{ display: 'block', fontSize: 12.5 }}>{meta.model?.name ?? ref.model}</span>
1133
- <span style={{ display: 'block', fontSize: 10.5, opacity: 0.58, fontFamily: 'monospace' }}>{ref.provider} / {ref.model}</span>
1134
- <span style={{ display: 'block', fontSize: 10.5, color: health?.state === 'callable' ? '#3fb950' : health ? '#c98735' : 'inherit', opacity: health ? 0.9 : 0.5 }}>
1135
- {healthLabel(health)}{healthAge(health) ? ` · ${healthAge(health)}` : ''}
1136
- </span>
1223
+ <span style={{ display: 'block', fontSize: 10.5, opacity: 0.58, fontFamily: 'monospace' }}>{ref.provider} / {ref.model}</span>
1224
+ <span style={{ display: 'block', fontSize: 10.5, color: health?.state === 'callable' ? '#3fb950' : health ? '#c98735' : 'inherit', opacity: health ? 0.9 : 0.5 }}>
1225
+ {healthLabel(health)}{healthAge(health) ? ` · ${healthAge(health)}` : ''}
1226
+ </span>
1137
1227
  {warning && <span style={{ display: 'block', fontSize: 10.5, color: '#c98735' }}>{warning}</span>}
1138
1228
  </span>
1139
1229
  <button type="button" style={{ ...S.btn, padding: '2px 7px' }} title={copy.moveUp} disabled={index === 0}
@@ -1225,31 +1315,31 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1225
1315
  <CustomSelect value={config.review_state ?? 'auto'} onChange={(v) => field('review_state', v)}
1226
1316
  options={(['auto', 'manual', 'disabled'] as const).map((s) => ({ value: s, label: copy.tierStateDesc[s] }))} /></label>
1227
1317
  </>))}
1228
- {block({ t: copy.autoReview, d: copy.autoReviewHint }, (
1229
- <>
1230
- <label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 6 }}>
1231
- <input type="checkbox" checked={config.auto_review ?? !!config.pro_reviews_flash} onChange={(e) => field('auto_review', e.target.checked)} />
1232
- <span style={{ fontSize: 12.5 }}>{copy.autoReview}</span>
1233
- </label>
1234
- <label style={S.field}><span style={S.fieldLabel}>{copy.reviewGate}</span>
1235
- <CustomSelect value={config.review?.gate ?? 'required'} onChange={(v) => reviewField({ gate: v })}
1236
- options={(['required', 'optional', 'off'] as const).map((value) => ({ value, label: copy.reviewGateDesc[value] }))} /></label>
1237
- </>
1238
- ))}
1239
- {block({ t: copy.modelOrdering, d: copy.orderingHint }, (<>
1240
- {(['worker', 'review'] as const).map((role) => (
1241
- <div key={role} style={{ display: 'contents' }}>
1242
- <label style={S.field}><span style={S.fieldLabel}>{role === 'worker' ? copy.roleWorker : copy.roleReviewer} · {copy.modelOrdering}</span>
1243
- <CustomSelect value={config?.[role]?.model_policy?.ordering ?? 'manual'}
1244
- onChange={(v) => rolePolicyField(role, { ordering: v })}
1245
- options={(['manual', 'health-aware'] as const).map((value) => ({ value, label: copy.orderingDesc[value] }))} /></label>
1246
- <label style={S.field}><span style={S.fieldLabel}>{role === 'worker' ? copy.roleWorker : copy.roleReviewer} · {copy.healthGate}</span>
1247
- <CustomSelect value={config?.[role]?.model_policy?.health_gate ?? 'hard-failures'}
1248
- onChange={(v) => rolePolicyField(role, { health_gate: v })}
1249
- options={(['hard-failures', 'off'] as const).map((value) => ({ value, label: copy.healthGateDesc[value] }))} /></label>
1250
- </div>
1251
- ))}
1252
- </>))}
1318
+ {block({ t: copy.autoReview, d: copy.autoReviewHint }, (
1319
+ <>
1320
+ <label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 6 }}>
1321
+ <input type="checkbox" checked={config.auto_review ?? !!config.pro_reviews_flash} onChange={(e) => field('auto_review', e.target.checked)} />
1322
+ <span style={{ fontSize: 12.5 }}>{copy.autoReview}</span>
1323
+ </label>
1324
+ <label style={S.field}><span style={S.fieldLabel}>{copy.reviewGate}</span>
1325
+ <CustomSelect value={config.review?.gate ?? 'required'} onChange={(v) => reviewField({ gate: v })}
1326
+ options={(['required', 'optional', 'off'] as const).map((value) => ({ value, label: copy.reviewGateDesc[value] }))} /></label>
1327
+ </>
1328
+ ))}
1329
+ {block({ t: copy.modelOrdering, d: copy.orderingHint }, (<>
1330
+ {(['worker', 'review'] as const).map((role) => (
1331
+ <div key={role} style={{ display: 'contents' }}>
1332
+ <label style={S.field}><span style={S.fieldLabel}>{role === 'worker' ? copy.roleWorker : copy.roleReviewer} · {copy.modelOrdering}</span>
1333
+ <CustomSelect value={config?.[role]?.model_policy?.ordering ?? 'manual'}
1334
+ onChange={(v) => rolePolicyField(role, { ordering: v })}
1335
+ options={(['manual', 'health-aware'] as const).map((value) => ({ value, label: copy.orderingDesc[value] }))} /></label>
1336
+ <label style={S.field}><span style={S.fieldLabel}>{role === 'worker' ? copy.roleWorker : copy.roleReviewer} · {copy.healthGate}</span>
1337
+ <CustomSelect value={config?.[role]?.model_policy?.health_gate ?? 'hard-failures'}
1338
+ onChange={(v) => rolePolicyField(role, { health_gate: v })}
1339
+ options={(['hard-failures', 'off'] as const).map((value) => ({ value, label: copy.healthGateDesc[value] }))} /></label>
1340
+ </div>
1341
+ ))}
1342
+ </>))}
1253
1343
  {block({ t: copy.isolation, d: copy.isolationHint }, (<>
1254
1344
  <label style={S.field}><span style={S.fieldLabel}>{copy.isolation}</span>
1255
1345
  <CustomSelect value={config.isolation ?? 'worktree'} onChange={(v) => field('isolation', v)}
@@ -1274,7 +1364,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1274
1364
  <CustomSelect value={mode} onChange={(v) => field('collaboration_mode', v)}
1275
1365
  options={(['flash-only', 'pro-only', 'balanced', 'review-pipeline', 'custom'] as const).map((m) => ({ value: m, label: copy.collaborationModeDesc[m] }))} /></label>
1276
1366
  <label style={S.field} title={copy.workerProviderHint}><span style={S.fieldLabel}>{copy.workerProvider}</span>
1277
- <CustomSelect value={config.worker_provider_mode ?? 'follow-dsh'} onChange={(v) => field('worker_provider_mode', v)}
1367
+ <CustomSelect value={config.worker_provider_mode ?? 'deepseek-official'} onChange={(v) => field('worker_provider_mode', v)}
1278
1368
  options={(['follow-dsh', 'deepseek-official'] as const).map((m) => ({ value: m, label: copy.workerProviderDesc[m] }))} /></label>
1279
1369
  </>))}
1280
1370
  {block({ t: copy.workerModels, d: modelCatalog ? copy.modelPoolSummary(modelCatalog.provider_count ?? 0, modelCatalog.model_count ?? 0) : copy.catalogFailed }, (<>
@@ -1328,48 +1418,48 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1328
1418
  <label style={S.field}><span style={S.fieldLabel}>{copy.presetPro}</span>
1329
1419
  <CustomSelect value={config.preset_pro ?? 'default'} onChange={(v) => field('preset_pro', v)} options={presetOptions} /></label>
1330
1420
  </>))}
1331
- </CollapsibleSection>
1332
-
1333
- <CollapsibleSection sectionId="adaptive" title={copy.sectionNames.adaptive}
1334
- summary={sectionSummary(adaptive.enabled ? copy.adaptiveEnabled : 'off', `${adaptive.min_samples}/${adaptive.window_size}`, copy.adaptiveBoundary)}
1335
- expanded={!!expandedSections.adaptive} onToggle={() => toggleSection('adaptive')}>
1336
- {block({ t: copy.adaptiveTitle, d: copy.adaptiveHint }, (<>
1337
- <label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 7, gridColumn: '1 / -1' }}>
1338
- <input type="checkbox" checked={adaptive.enabled}
1339
- onChange={(event) => saveAdaptive({ ...adaptive, enabled: event.target.checked })} />
1340
- <span style={{ fontSize: 12.5 }}>{copy.adaptiveEnabled}</span>
1341
- <span style={{ fontSize: 11, opacity: 0.55 }}>· {copy.adaptiveBoundary}</span>
1342
- </label>
1343
- <label style={S.field}><span style={S.fieldLabel}>{copy.adaptiveWindow}</span>
1344
- <input type="number" min={1} max={32} value={adaptive.window_size} style={S.input}
1345
- onChange={(event) => setAdaptiveLocal({ ...adaptive, window_size: clampInt(event.target.value, adaptive.window_size, 1, 32) })}
1346
- onBlur={() => saveAdaptive(adaptive)} /></label>
1347
- <label style={S.field}><span style={S.fieldLabel}>{copy.adaptiveSamples}</span>
1348
- <input type="number" min={1} max={adaptive.window_size} value={adaptive.min_samples} style={S.input}
1349
- onChange={(event) => setAdaptiveLocal({ ...adaptive, min_samples: clampInt(event.target.value, adaptive.min_samples, 1, adaptive.window_size) })}
1350
- onBlur={() => saveAdaptive(adaptive)} /></label>
1351
- </>))}
1352
- </CollapsibleSection>
1353
-
1354
- <CollapsibleSection sectionId="runtime" title={copy.sectionNames.runtime}
1421
+ </CollapsibleSection>
1422
+
1423
+ <CollapsibleSection sectionId="adaptive" title={copy.sectionNames.adaptive}
1424
+ summary={sectionSummary(adaptive.enabled ? copy.adaptiveEnabled : 'off', `${adaptive.min_samples}/${adaptive.window_size}`, copy.adaptiveBoundary)}
1425
+ expanded={!!expandedSections.adaptive} onToggle={() => toggleSection('adaptive')}>
1426
+ {block({ t: copy.adaptiveTitle, d: copy.adaptiveHint }, (<>
1427
+ <label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 7, gridColumn: '1 / -1' }}>
1428
+ <input type="checkbox" checked={adaptive.enabled}
1429
+ onChange={(event) => saveAdaptive({ ...adaptive, enabled: event.target.checked })} />
1430
+ <span style={{ fontSize: 12.5 }}>{copy.adaptiveEnabled}</span>
1431
+ <span style={{ fontSize: 11, opacity: 0.55 }}>· {copy.adaptiveBoundary}</span>
1432
+ </label>
1433
+ <label style={S.field}><span style={S.fieldLabel}>{copy.adaptiveWindow}</span>
1434
+ <input type="number" min={1} max={32} value={adaptive.window_size} style={S.input}
1435
+ onChange={(event) => setAdaptiveLocal({ ...adaptive, window_size: clampInt(event.target.value, adaptive.window_size, 1, 32) })}
1436
+ onBlur={() => saveAdaptive(adaptive)} /></label>
1437
+ <label style={S.field}><span style={S.fieldLabel}>{copy.adaptiveSamples}</span>
1438
+ <input type="number" min={1} max={adaptive.window_size} value={adaptive.min_samples} style={S.input}
1439
+ onChange={(event) => setAdaptiveLocal({ ...adaptive, min_samples: clampInt(event.target.value, adaptive.min_samples, 1, adaptive.window_size) })}
1440
+ onBlur={() => saveAdaptive(adaptive)} /></label>
1441
+ </>))}
1442
+ </CollapsibleSection>
1443
+
1444
+ <CollapsibleSection sectionId="runtime" title={copy.sectionNames.runtime}
1355
1445
  summary={sectionSummary(config.mode, `${config.default_timeout_seconds}s`, config.hub_url)}
1356
1446
  expanded={!!expandedSections.runtime} onToggle={() => toggleSection('runtime')}>
1357
1447
  {block(copy.cardRuntime, (<>
1358
1448
  <label style={S.field}><span style={S.fieldLabel}>{copy.mode}</span>
1359
1449
  <CustomSelect value={config.mode} onChange={(v) => field('mode', v)}
1360
- options={(['auto', 'hub'] as const).map((m) => ({ value: m, label: copy.modeDesc[m] }))} /></label>
1450
+ options={(['auto', 'hub'] as const).map((m) => ({ value: m, label: copy.modeDesc[m] }))} /></label>
1361
1451
  <label style={S.field}><span style={S.fieldLabel}>{copy.timeout}</span>
1362
1452
  <input style={{ ...S.input, width: '100%', boxSizing: 'border-box' as const }} type="number" min={60} max={7200} value={config.default_timeout_seconds}
1363
1453
  onChange={(e) => fieldLocal('default_timeout_seconds', Number(e.target.value))}
1364
- onBlur={() => { void applyPatch({ default_timeout_seconds: config.default_timeout_seconds }); }} /></label>
1454
+ onBlur={() => { void applyPatch({ default_timeout_seconds: config.default_timeout_seconds }); }} /></label>
1365
1455
  <label style={S.field}><span style={S.fieldLabel}>{copy.hubUrl}</span>
1366
1456
  <input style={{ ...S.input, width: '100%', boxSizing: 'border-box' as const }} value={config.hub_url}
1367
1457
  onChange={(e) => fieldLocal('hub_url', e.target.value)}
1368
- onBlur={() => { void applyPatch({ hub_url: config.hub_url }); }}
1458
+ onBlur={() => { void applyPatch({ hub_url: config.hub_url }); }}
1369
1459
  onKeyDown={(e: any) => { if (e.key === 'Enter') e.currentTarget.blur(); }} /></label>
1370
- </>))}
1371
- <ActivationSummary activation={config.config_activation} locale={locale} />
1372
- </CollapsibleSection>
1460
+ </>))}
1461
+ <ActivationSummary activation={config.config_activation} locale={locale} />
1462
+ </CollapsibleSection>
1373
1463
 
1374
1464
  <CollapsibleSection sectionId="multimodal" title={copy.sectionNames.multimodal}
1375
1465
  summary={sectionSummary(config.vision_enabled ? `${copy.capVision}: ${config.vision_provider}` : `${copy.capVision}: off`, config.imagegen_enabled ? `${copy.capImagegen}: ${config.imagegen_provider}` : `${copy.capImagegen}: off`)}
@@ -1387,7 +1477,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1387
1477
  const m = custom ? (custom.models?.[0] ?? 'default') : (VISION_MODELS[p] ?? ['default'])[0];
1388
1478
  setModelAdd(null);
1389
1479
  setConfig((c: any) => ({ ...c, vision_provider: p, vision_model: m }));
1390
- void applyPatch({ vision_provider: p, vision_model: m });
1480
+ void applyPatch({ vision_provider: p, vision_model: m });
1391
1481
  if ((p === 'grok' || p === 'agy') && !dynModels[p]) {
1392
1482
  void get(`/vision-models?provider=${p}`).then((r) => {
1393
1483
  if (r.ok) setDynModels((d) => ({ ...d, [p]: r.models }));
@@ -1442,69 +1532,116 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1442
1532
  </>))}
1443
1533
  </CollapsibleSection>
1444
1534
 
1445
- <CollapsibleSection sectionId="harnessProviders" title={copy.sectionNames.harnessProviders}
1446
- summary={sectionSummary(copy.providerCount(providerInventory?.records?.length ?? 0), providerInventoryError ? copy.providerLifecycleError : '')}
1447
- expanded={!!expandedSections.harnessProviders} onToggle={() => toggleSection('harnessProviders')}>
1448
- {providerInventoryError && <div style={{ fontSize: 12, color: '#c55' }}>{providerInventoryError}</div>}
1449
- {!providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && (
1450
- <div style={{ fontSize: 12, opacity: 0.55 }}>{copy.providerNoInventory}</div>
1451
- )}
1452
- {(providerInventory?.records ?? []).map((record: any) => {
1453
- const blocked = record.ownership === 'harness' || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0;
1454
- const lifecycle = record.desired_state === 'absent' ? 'absent' : record.lifecycle?.enabled === false ? 'disabled' : record.lifecycle?.catalogued ? 'catalogued' : 'configured';
1455
- const rollbackTransaction = (providerInventory?.lifecycle_transactions ?? []).find((entry: any) => entry.provider_id === record.id && ['VERIFIED', 'RESTART_PENDING'].includes(entry.state));
1456
- return (
1457
- <div key={record.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, flexWrap: 'wrap' as const }}>
1458
- <span style={{ fontWeight: 600 }}>{record.display_name || record.id}</span>
1459
- <span style={{ ...S.chip(record.lifecycle?.enabled === true), ...S.mono }}>{record.id}</span>
1460
- <span style={{ opacity: 0.65 }}>{copy.providerState(lifecycle)}</span>
1461
- <span style={{ opacity: 0.65 }}>{copy.providerModels(record.models?.length ?? 0)}</span>
1462
- {Number(record.references?.active_jobs ?? 0) > 0 && <span style={{ color: '#c98735' }}>{copy.providerJobs(record.references.active_jobs)}</span>}
1463
- <span style={{ flex: 1 }} />
1464
- {record.ownership !== 'harness' && record.desired_state !== 'absent' && (
1465
- <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={blocked || providerLifecycleBusy !== null}
1466
- onClick={() => { void deleteHarnessProvider(record); }}>
1467
- {providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan}
1468
- </button>
1469
- )}
1470
- {record.desired_state === 'absent' && rollbackTransaction && (
1471
- <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
1472
- onClick={() => { void rollbackHarnessProvider(record, rollbackTransaction.transaction_id); }}>
1473
- {providerLifecycleBusy === `rollback:${record.id}` ? copy.working : copy.providerRollback}
1474
- </button>
1475
- )}
1476
- </div>
1477
- );
1478
- })}
1479
- {(credentialRefs.length > 0 || credentialUnverified.length > 0) && (
1480
- <div style={{ marginTop: 10, borderTop: '1px solid rgba(128,128,128,0.16)', paddingTop: 8 }}>
1481
- <div style={{ fontWeight: 600, fontSize: 12.5 }}>{copy.credentialReferences}</div>
1482
- <div style={{ fontSize: 11, opacity: 0.6, margin: '3px 0 7px' }}>{copy.credentialReferenceHint}</div>
1483
- {credentialUnverified.map((entry: any) => (
1484
- <div key={`unverified:${entry.reference_id}`} style={{ color: '#c55', fontSize: 11.5 }}>
1485
- <span style={S.mono}>{entry.reference_id}</span> · {copy.credentialPurgeUnverified}
1486
- </div>
1487
- ))}
1488
- {credentialRefs.map((ref: any) => (
1489
- <div key={ref.reference_id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11.5, flexWrap: 'wrap' as const }}>
1490
- <span style={S.mono}>{ref.reference_id}</span>
1491
- <span style={{ opacity: 0.65 }}>{ref.ownership}</span>
1492
- <span style={{ color: ref.orphan ? '#c98735' : undefined }}>{ref.orphan ? copy.credentialOrphan : copy.credentialInUse(ref.consumer_count ?? 0)}</span>
1493
- <span style={{ flex: 1 }} />
1494
- {ref.ownership === 'crew-owned' && ref.orphan === true && ref.purge_capability === 'eligible' && (
1495
- <button type="button" style={{ ...S.btn, padding: '2px 8px' }}
1496
- disabled={credentialLifecycleBusy !== null}
1497
- onClick={() => { void purgeCredentialRef(ref); }}>
1498
- {credentialLifecycleBusy === ref.reference_id ? copy.credentialPurging : copy.credentialPurgePlan}
1499
- </button>
1500
- )}
1501
- </div>
1502
- ))}
1503
- </div>
1504
- )}
1505
- </CollapsibleSection>
1506
-
1507
- <CollapsibleSection sectionId="providers" title={copy.sectionNames.providers}
1535
+ <CollapsibleSection sectionId="harnessProviders" title={copy.sectionNames.harnessProviders}
1536
+ summary={sectionSummary(copy.providerCount(providerInventory?.records?.length ?? 0), providerInventoryError ? copy.providerLifecycleError : '')}
1537
+ expanded={!!expandedSections.harnessProviders} onToggle={() => toggleSection('harnessProviders')}>
1538
+ {providerInventoryError && <div style={{ fontSize: 12, color: '#c55' }}>{providerInventoryError}</div>}
1539
+ {!providerInventoryError && (providerInventory?.migration?.providers?.length ?? 0) > 0 && (
1540
+ <div style={{ border: '1px solid rgba(201,135,53,0.45)', borderRadius: 7, padding: '8px 10px', marginBottom: 8 }}>
1541
+ <div style={{ fontWeight: 650, fontSize: 12.5 }}>{copy.providerLayerMigration}</div>
1542
+ <div style={{ fontSize: 11.5, opacity: 0.7, marginTop: 3 }}>{copy.providerLayerMigrationHint}</div>
1543
+ <div style={{ fontSize: 11, marginTop: 6, fontFamily: 'monospace' }}>
1544
+ {(providerInventory.migration.providers ?? []).map((entry: any) => (
1545
+ <div key={entry.provider_id} style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 3 }}>
1546
+ {entry.provider_id} · {entry.action}{entry.harness_default ? ' · Harness Default' : ''}
1547
+ <span style={{ flex: 1 }} />
1548
+ {['promote-existing-user', 'materialize-user'].includes(entry.action) && <button type="button" style={{ ...S.btn, padding: '2px 7px' }} disabled={providerLifecycleBusy !== null}
1549
+ onClick={() => { void migrateHarnessProvider(entry); }}>
1550
+ {providerLifecycleBusy === `migrate:${entry.provider_id}` ? copy.providerLayerMigrating : copy.providerLayerMigrate}
1551
+ </button>}
1552
+ </div>
1553
+ ))}
1554
+ </div>
1555
+ <a href="http://127.0.0.1:3210/" target="_blank" rel="noopener noreferrer" style={{ ...S.btn, display: 'inline-block', textDecoration: 'none', marginTop: 7, padding: '3px 8px' }}>
1556
+ {copy.providerLayerMigrationPlan} → 3210
1557
+ </a>
1558
+ </div>
1559
+ )}
1560
+ {!providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && (
1561
+ <div style={{ fontSize: 12, opacity: 0.55 }}>{copy.providerNoInventory}</div>
1562
+ )}
1563
+ {(providerInventory?.records ?? []).map((record: any) => {
1564
+ const immutable = record.delete_capability === 'immutable-builtin';
1565
+ const unresolved = record.delete_capability === 'source-unresolved';
1566
+ const blocked = immutable || unresolved || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0;
1567
+ const lifecycle = record.desired_state === 'absent' ? 'absent' : record.lifecycle?.enabled === false ? 'disabled' : record.lifecycle?.catalogued ? 'catalogued' : 'configured';
1568
+ const lifecycleTransactions = providerInventory?.lifecycle_transactions ?? [];
1569
+ const recoveryTransactions = (providerInventory?.recovery_transactions ?? []).map((entry: any) => ({ ...entry, state: entry.phase }));
1570
+ const rollbackTransaction = [...lifecycleTransactions, ...recoveryTransactions]
1571
+ .filter((entry: any) => entry.provider_id === record.id && ['VERIFIED', 'RESTART_PENDING', 'ROLLBACK_PENDING', 'PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_RESTORED', 'ROLLBACK_APPLYING', 'PREPARED', 'USER_MATERIALIZED', 'BASE_REMOVED'].includes(entry.state))
1572
+ .sort((a: any, b: any) => String(a.updated_at ?? '').localeCompare(String(b.updated_at ?? ''))).at(-1);
1573
+ return (
1574
+ <div key={record.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, flexWrap: 'wrap' as const }}>
1575
+ <span style={{ fontWeight: 600 }}>{record.display_name || record.id}</span>
1576
+ <span style={{ ...S.chip(record.lifecycle?.enabled === true), ...S.mono }}>{record.id}</span>
1577
+ <span style={{ opacity: 0.65 }}>{copy.providerState(lifecycle)}</span>
1578
+ <span style={{ opacity: 0.65 }}>{copy.providerModels(record.models?.length ?? 0)}</span>
1579
+ {immutable && <span style={{ opacity: 0.65 }}>{copy.providerOfficialBuiltin}</span>}
1580
+ {unresolved && <span style={{ color: '#c98735' }}>{copy.providerSourceUnresolved}</span>}
1581
+ {Number(record.references?.active_jobs ?? 0) > 0 && <span style={{ color: '#c98735' }}>{copy.providerJobs(record.references.active_jobs)}</span>}
1582
+ <span style={{ flex: 1 }} />
1583
+ {record.delete_capability === 'supported' && record.desired_state !== 'absent' && (
1584
+ <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={blocked || providerLifecycleBusy !== null}
1585
+ onClick={() => { void deleteHarnessProvider(record); }}>
1586
+ {providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan}
1587
+ </button>
1588
+ )}
1589
+ {(record.desired_state === 'absent' || ['PLANNED', 'TOMBSTONE_APPLYING', 'TOMBSTONE_APPLIED', 'REFERENCES_APPLYING', 'REFERENCES_APPLIED', 'DECLARATIONS_APPLYING', 'DECLARATIONS_APPLIED', 'DELETE_RESTART_PENDING', 'ROLLBACK_PENDING', 'ROLLBACK_RESTART_PENDING', 'ROLLBACK_RESTORED', 'ROLLBACK_APPLYING'].includes(rollbackTransaction?.state)) && rollbackTransaction && rollbackTransaction.source !== 'provider-layer-migration' && (
1590
+ <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
1591
+ onClick={() => { void rollbackHarnessProvider(record, rollbackTransaction.transaction_id, rollbackTransaction.state); }}>
1592
+ {providerLifecycleBusy === `rollback:${record.id}` ? copy.working : rollbackTransaction.state === 'ROLLBACK_PENDING' ? copy.providerRollbackContinue : copy.providerRollback}
1593
+ </button>
1594
+ )}
1595
+ {rollbackTransaction?.source === 'provider-layer-migration' && rollbackTransaction.transaction_id && (
1596
+ <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
1597
+ onClick={() => { void rollbackHarnessMigration(record, rollbackTransaction.transaction_id); }}>
1598
+ {providerLifecycleBusy === `rollback-migrate:${record.id}` ? copy.working : copy.providerLayerRollback}
1599
+ </button>
1600
+ )}
1601
+ </div>
1602
+ );
1603
+ })}
1604
+ {(providerInventory?.recovery_transactions ?? []).filter((entry: any) => entry?.unresolved === true || !entry?.provider_id).map((entry: any) => (
1605
+ <div key={`recovery:${entry.storage_id ?? entry.transaction_id ?? 'unknown'}`} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, flexWrap: 'wrap' as const, color: '#c98735', marginTop: 6 }}>
1606
+ <span style={{ fontWeight: 600 }}>{copy.recoveryUnresolved}</span>
1607
+ <span style={S.mono}>{entry.storage_id ?? entry.transaction_id ?? 'unknown'}</span>
1608
+ <span style={{ opacity: 0.7 }}>{entry.phase}</span>
1609
+ <span style={{ flex: 1 }} />
1610
+ {entry.action_id && <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={providerLifecycleBusy !== null}
1611
+ onClick={() => { void quarantineRecoveryTransaction(entry); }}>
1612
+ {providerLifecycleBusy === `quarantine:${entry.action_id}` ? copy.working : copy.recoveryQuarantine}
1613
+ </button>}
1614
+ </div>
1615
+ ))}
1616
+ {(credentialRefs.length > 0 || credentialUnverified.length > 0) && (
1617
+ <div style={{ marginTop: 10, borderTop: '1px solid rgba(128,128,128,0.16)', paddingTop: 8 }}>
1618
+ <div style={{ fontWeight: 600, fontSize: 12.5 }}>{copy.credentialReferences}</div>
1619
+ <div style={{ fontSize: 11, opacity: 0.6, margin: '3px 0 7px' }}>{copy.credentialReferenceHint}</div>
1620
+ {credentialUnverified.map((entry: any) => (
1621
+ <div key={`unverified:${entry.reference_id}`} style={{ color: '#c55', fontSize: 11.5 }}>
1622
+ <span style={S.mono}>{entry.reference_id}</span> · {copy.credentialPurgeUnverified}
1623
+ </div>
1624
+ ))}
1625
+ {credentialRefs.map((ref: any) => (
1626
+ <div key={ref.reference_id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11.5, flexWrap: 'wrap' as const }}>
1627
+ <span style={S.mono}>{ref.reference_id}</span>
1628
+ <span style={{ opacity: 0.65 }}>{ref.ownership}</span>
1629
+ <span style={{ color: ref.orphan ? '#c98735' : undefined }}>{ref.orphan ? copy.credentialOrphan : copy.credentialInUse(ref.consumer_count ?? 0)}</span>
1630
+ <span style={{ flex: 1 }} />
1631
+ {ref.ownership === 'crew-owned' && ref.orphan === true && ref.purge_capability === 'eligible' && (
1632
+ <button type="button" style={{ ...S.btn, padding: '2px 8px' }}
1633
+ disabled={credentialLifecycleBusy !== null}
1634
+ onClick={() => { void purgeCredentialRef(ref); }}>
1635
+ {credentialLifecycleBusy === ref.reference_id ? copy.credentialPurging : copy.credentialPurgePlan}
1636
+ </button>
1637
+ )}
1638
+ </div>
1639
+ ))}
1640
+ </div>
1641
+ )}
1642
+ </CollapsibleSection>
1643
+
1644
+ <CollapsibleSection sectionId="providers" title={copy.sectionNames.providers}
1508
1645
  summary={sectionSummary(copy.providerCount(customProviders.length), testResult?.ok === false && copy.testFail)}
1509
1646
  expanded={!!expandedSections.providers} onToggle={() => toggleSection('providers')}>
1510
1647
  {block(copy.cardCustomProv, (<>
@@ -1611,80 +1748,80 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1611
1748
  <CollapsibleSection sectionId="jobs" title={copy.sectionNames.jobs}
1612
1749
  summary={sectionSummary(copy.jobCount(jobs.length), jobs.some((job) => job.status === 'running') && copy.runningCount(jobs.filter((job) => job.status === 'running').length))}
1613
1750
  expanded={!!expandedSections.jobs} onToggle={() => toggleSection('jobs')}>
1614
- {jobs.length === 0 ? (
1615
- <div style={{ opacity: 0.55 }}>{copy.empty}</div>
1616
- ) : (
1617
- <div style={{ overflowX: 'auto', border: '1px solid rgba(128,128,128,0.20)', borderRadius: 8, padding: '0 9px' }}>
1618
- <table style={{ borderCollapse: 'collapse', width: '100%', tableLayout: 'fixed', minWidth: 1020 }}>
1619
- <colgroup>
1620
- <col style={{ width: 68 }} /><col style={{ width: 72 }} /><col style={{ width: 58 }} />
1621
- <col style={{ width: 190 }} /><col style={{ width: 70 }} /><col style={{ width: 70 }} />
1622
- <col style={{ width: 105 }} /><col style={{ width: 84 }} /><col style={{ width: 300 }} />
1623
- </colgroup>
1624
- <thead><tr>
1625
- {[copy.col.id, copy.col.role, copy.col.source, copy.col.model, copy.col.tier, copy.col.status, copy.col.progress, copy.col.tokens, copy.col.task].map((heading) => (
1626
- <th key={heading} style={{ ...S.tight, fontSize: 10.5, opacity: 0.55, fontWeight: 500 }}>{heading}</th>
1627
- ))}
1628
- </tr></thead>
1629
- <tbody>
1630
- {jobs.map((job) => {
1631
- const color = job.status === 'running' ? '#4a9eff' : job.status === 'done' ? '#3fb950' : '#f85149';
1632
- return (
1633
- <tr key={job.id}>
1634
- <td style={{ ...S.tight, ...S.mono, fontWeight: 600 }} title={job.id}>{String(job.id).replace(/-[a-z0-9]+$/, '')}</td>
1635
- <td style={S.tight}><span style={S.chip(job.role === 'reviewer')}>{job.role === 'reviewer' ? 'Reviewer' : 'Worker'}</span></td>
1636
- <td style={S.tight}><span style={S.chip(job.source === 'claude-code' || job.source === 'codex')}>{sourceLabel(job.source)}</span></td>
1637
- <td style={{ ...S.tight, ...S.mono }} title={`${job.provider ?? '—'} / ${job.model ?? '—'}`}>{job.provider ?? '—'} / {job.model ?? '—'}</td>
1638
- <td style={S.tight}>{job.tier}/{job.effort}</td>
1639
- <td style={S.tight} title={`${job.status}${job.currentTool ? ` · ${job.currentTool}` : ''}`}>
1640
- <span style={{ color }} aria-hidden="true">●</span> <span style={{ color, fontSize: 10.5 }}>{job.status}</span>
1641
- </td>
1642
- <td style={{ ...S.tight, ...S.mono }} title={copy.progressTip(elapsed(job.startedAt, job.endedAt), job.turn, job.step, job.toolCalls ?? '-')}>{elapsed(job.startedAt, job.endedAt)} · {job.turn}.{job.step} · {job.toolCalls ?? '-'}t</td>
1643
- <td style={{ ...S.tight, ...S.mono }}>{job.tokens ? `${ktok(job.tokens.input)}/${ktok(job.tokens.output)}` : '—'}</td>
1644
- <td
1645
- style={{ ...S.cell, position: 'relative' }}
1646
- onMouseEnter={(event: any) => {
1647
- const rect = event.currentTarget.getBoundingClientRect();
1648
- setHoverTask({ id: job.id, text: job.task, right: Math.max(8, window.innerWidth - rect.right), top: rect.bottom + 4, bottom: window.innerHeight - rect.top + 4, flip: rect.top < 240 });
1649
- }}
1650
- onMouseLeave={() => setHoverTask(null)}>
1651
- <div style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: 1.45, fontSize: 12 }}>
1652
- {job.task}
1653
- </div>
1654
- <div style={{ fontSize: 10, opacity: 0.5 }}>{job.selection_source ?? 'unknown'}</div>
1655
- </td>
1656
- </tr>
1657
- );
1658
- })}
1659
- </tbody>
1660
- </table>
1661
- </div>
1662
- )}
1663
-
1664
- <div style={{ ...S.block, marginTop: 2 }}>
1665
- <div>
1666
- <div style={S.settingTitle}>{copy.modelActivity}</div>
1667
- <div style={S.settingDesc}>{copy.modelActivityHint}</div>
1668
- </div>
1669
- {modelActivity.length === 0 ? <div style={{ opacity: 0.55 }}>{copy.noModelActivity}</div> : (
1670
- <div style={{ overflowX: 'auto' }}>
1671
- <div role="table" style={{ minWidth: 620 }}>
1672
- <div role="row" style={{ display: 'grid', gridTemplateColumns: 'minmax(190px, 1.35fr) 72px minmax(170px, 1fr) 125px', gap: 10, padding: '0 6px 5px', fontSize: 10.5, opacity: 0.55 }}>
1673
- <span role="columnheader">{copy.model}</span><span role="columnheader">{copy.calls}</span><span role="columnheader">{copy.invocationSource} / {copy.routingSource}</span><span role="columnheader">{copy.lastCalled}</span>
1674
- </div>
1675
- {modelActivity.map((entry) => (
1676
- <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)' }}>
1677
- <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>
1678
- <span role="cell" style={{ ...S.mono, fontSize: 15, fontWeight: 650 }}>{entry.count}</span>
1679
- <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>
1680
- <span role="cell" style={{ ...S.mono, fontSize: 10.5 }}>{formatTimestamp(entry.last_called_at, locale)}</span>
1681
- </div>
1682
- ))}
1683
- </div>
1684
- </div>
1685
- )}
1686
- </div>
1687
- </CollapsibleSection>
1751
+ {jobs.length === 0 ? (
1752
+ <div style={{ opacity: 0.55 }}>{copy.empty}</div>
1753
+ ) : (
1754
+ <div style={{ overflowX: 'auto', border: '1px solid rgba(128,128,128,0.20)', borderRadius: 8, padding: '0 9px' }}>
1755
+ <table style={{ borderCollapse: 'collapse', width: '100%', tableLayout: 'fixed', minWidth: 1020 }}>
1756
+ <colgroup>
1757
+ <col style={{ width: 68 }} /><col style={{ width: 72 }} /><col style={{ width: 58 }} />
1758
+ <col style={{ width: 190 }} /><col style={{ width: 70 }} /><col style={{ width: 70 }} />
1759
+ <col style={{ width: 105 }} /><col style={{ width: 84 }} /><col style={{ width: 300 }} />
1760
+ </colgroup>
1761
+ <thead><tr>
1762
+ {[copy.col.id, copy.col.role, copy.col.source, copy.col.model, copy.col.tier, copy.col.status, copy.col.progress, copy.col.tokens, copy.col.task].map((heading) => (
1763
+ <th key={heading} style={{ ...S.tight, fontSize: 10.5, opacity: 0.55, fontWeight: 500 }}>{heading}</th>
1764
+ ))}
1765
+ </tr></thead>
1766
+ <tbody>
1767
+ {jobs.map((job) => {
1768
+ const color = job.status === 'running' ? '#4a9eff' : job.status === 'done' ? '#3fb950' : '#f85149';
1769
+ return (
1770
+ <tr key={job.id}>
1771
+ <td style={{ ...S.tight, ...S.mono, fontWeight: 600 }} title={job.id}>{String(job.id).replace(/-[a-z0-9]+$/, '')}</td>
1772
+ <td style={S.tight}><span style={S.chip(job.role === 'reviewer')}>{job.role === 'reviewer' ? 'Reviewer' : 'Worker'}</span></td>
1773
+ <td style={S.tight}><span style={S.chip(job.source === 'claude-code' || job.source === 'codex')}>{sourceLabel(job.source)}</span></td>
1774
+ <td style={{ ...S.tight, ...S.mono }} title={`${job.provider ?? '—'} / ${job.model ?? '—'}`}>{job.provider ?? '—'} / {job.model ?? '—'}</td>
1775
+ <td style={S.tight}>{job.tier}/{job.effort}</td>
1776
+ <td style={S.tight} title={`${job.status}${job.currentTool ? ` · ${job.currentTool}` : ''}`}>
1777
+ <span style={{ color }} aria-hidden="true">●</span> <span style={{ color, fontSize: 10.5 }}>{job.status}</span>
1778
+ </td>
1779
+ <td style={{ ...S.tight, ...S.mono }} title={copy.progressTip(elapsed(job.startedAt, job.endedAt), job.turn, job.step, job.toolCalls ?? '-')}>{elapsed(job.startedAt, job.endedAt)} · {job.turn}.{job.step} · {job.toolCalls ?? '-'}t</td>
1780
+ <td style={{ ...S.tight, ...S.mono }}>{job.tokens ? `${ktok(job.tokens.input)}/${ktok(job.tokens.output)}` : '—'}</td>
1781
+ <td
1782
+ style={{ ...S.cell, position: 'relative' }}
1783
+ onMouseEnter={(event: any) => {
1784
+ const rect = event.currentTarget.getBoundingClientRect();
1785
+ setHoverTask({ id: job.id, text: job.task, right: Math.max(8, window.innerWidth - rect.right), top: rect.bottom + 4, bottom: window.innerHeight - rect.top + 4, flip: rect.top < 240 });
1786
+ }}
1787
+ onMouseLeave={() => setHoverTask(null)}>
1788
+ <div style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: 1.45, fontSize: 12 }}>
1789
+ {job.task}
1790
+ </div>
1791
+ <div style={{ fontSize: 10, opacity: 0.5 }}>{job.selection_source ?? 'unknown'}</div>
1792
+ </td>
1793
+ </tr>
1794
+ );
1795
+ })}
1796
+ </tbody>
1797
+ </table>
1798
+ </div>
1799
+ )}
1800
+
1801
+ <div style={{ ...S.block, marginTop: 2 }}>
1802
+ <div>
1803
+ <div style={S.settingTitle}>{copy.modelActivity}</div>
1804
+ <div style={S.settingDesc}>{copy.modelActivityHint}</div>
1805
+ </div>
1806
+ {modelActivity.length === 0 ? <div style={{ opacity: 0.55 }}>{copy.noModelActivity}</div> : (
1807
+ <div style={{ overflowX: 'auto' }}>
1808
+ <div role="table" style={{ minWidth: 620 }}>
1809
+ <div role="row" style={{ display: 'grid', gridTemplateColumns: 'minmax(190px, 1.35fr) 72px minmax(170px, 1fr) 125px', gap: 10, padding: '0 6px 5px', fontSize: 10.5, opacity: 0.55 }}>
1810
+ <span role="columnheader">{copy.model}</span><span role="columnheader">{copy.calls}</span><span role="columnheader">{copy.invocationSource} / {copy.routingSource}</span><span role="columnheader">{copy.lastCalled}</span>
1811
+ </div>
1812
+ {modelActivity.map((entry) => (
1813
+ <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)' }}>
1814
+ <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>
1815
+ <span role="cell" style={{ ...S.mono, fontSize: 15, fontWeight: 650 }}>{entry.count}</span>
1816
+ <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>
1817
+ <span role="cell" style={{ ...S.mono, fontSize: 10.5 }}>{formatTimestamp(entry.last_called_at, locale)}</span>
1818
+ </div>
1819
+ ))}
1820
+ </div>
1821
+ </div>
1822
+ )}
1823
+ </div>
1824
+ </CollapsibleSection>
1688
1825
  {hoverTask && createPortal(
1689
1826
  <div style={{
1690
1827
  position: 'fixed', right: hoverTask.right, zIndex: 9999,
@@ -1702,9 +1839,9 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1702
1839
  );
1703
1840
  }
1704
1841
 
1705
- export function apply(ctx: any): void {
1706
- let runningCount = 0;
1707
- let officialSurface: boolean | null = null;
1842
+ export function apply(ctx: any): void {
1843
+ let runningCount = 0;
1844
+ let officialSurface: boolean | null = null;
1708
1845
  ctx.slots.inject('settings.section', () => {
1709
1846
  let dispose = register();
1710
1847
  function register() {
@@ -1725,16 +1862,16 @@ export function apply(ctx: any): void {
1725
1862
  () => <WorkersPanel ctx={ctx} />,
1726
1863
  );
1727
1864
  }
1728
- const poll = async () => {
1729
- if (document.visibilityState !== 'visible') return;
1730
- try {
1731
- if (officialSurface === null) {
1732
- const response = await fetch(`${API}/bridge-status`, { cache: 'no-store' });
1733
- const bridgeStatus = response.ok ? await response.json() : null;
1734
- officialSurface = classifyCrewSurface({ bridgeStatus }) === CREW_UI_SURFACES.OFFICIAL;
1735
- }
1736
- if (!officialSurface) return;
1737
- const r = await (await fetch(`${API}/jobs`, { cache: 'no-store' })).json();
1865
+ const poll = async () => {
1866
+ if (document.visibilityState !== 'visible') return;
1867
+ try {
1868
+ if (officialSurface === null) {
1869
+ const response = await fetch(`${API}/bridge-status`, { cache: 'no-store' });
1870
+ const bridgeStatus = response.ok ? await response.json() : null;
1871
+ officialSurface = classifyCrewSurface({ bridgeStatus }) === CREW_UI_SURFACES.OFFICIAL;
1872
+ }
1873
+ if (!officialSurface) return;
1874
+ const r = await (await fetch(`${API}/jobs`, { cache: 'no-store' })).json();
1738
1875
  const n = r.ok ? (r.jobs ?? []).filter((j: any) => j.status === 'running').length : 0;
1739
1876
  if (n !== runningCount) {
1740
1877
  runningCount = n;