@ran-sh/dsh-crew 0.5.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/README.zh.md +18 -7
- package/docs/installation.md +1 -1
- package/docs/ui-surfaces.md +65 -46
- package/lib/client.js +565 -28
- package/official-web-bridge/lib/client.js +565 -28
- package/package.json +1 -1
- package/src/client/collapsible-sections.mjs +3 -2
- package/src/client/host-readiness.mjs +16 -3
- package/src/client/index.tsx +331 -54
- package/src/config-readiness.mjs +66 -5
- package/src/credential-lifecycle.mjs +73 -0
- package/src/credential-purge-adapters.mjs +127 -0
- package/src/credential-purge-state.mjs +59 -0
- package/src/credential-reference-inventory.mjs +78 -0
- package/src/dsh-cli-runtime.mjs +36 -9
- package/src/extension-contract.mjs +16 -10
- package/src/hub/index.mjs +1004 -438
- package/src/hub-client.mjs +25 -5
- package/src/hub-compatibility.mjs +23 -7
- package/src/install/install.mjs +4 -4
- package/src/install/npx-lifecycle.mjs +495 -134
- package/src/job-contracts.mjs +27 -6
- package/src/jobs.mjs +2 -4
- package/src/local-request-guard.mjs +60 -0
- package/src/mcp-runtime.mjs +7 -5
- package/src/model-routing.mjs +141 -49
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +248 -95
- package/src/policy.mjs +62 -31
- package/src/provider-config-scrub.mjs +71 -0
- package/src/provider-delete-adapters.mjs +451 -0
- package/src/provider-desired-state.mjs +37 -0
- package/src/provider-health.mjs +124 -0
- package/src/provider-inventory.mjs +132 -0
- package/src/provider-lifecycle-state.mjs +95 -0
- package/src/provider-lifecycle.mjs +214 -0
- package/src/provider-profile-store.mjs +171 -0
- package/src/readiness-matrix.mjs +11 -5
- package/src/removable-waiter.mjs +29 -0
- package/src/runtime-identity.mjs +66 -21
- package/src/runtime-readiness-snapshot.mjs +87 -0
- package/src/server.mjs +64 -21
- package/src/status-shard.mjs +13 -2
- package/src/workflow-runtime.mjs +44 -10
- package/windows/start-dsh-crew.cmd +3 -3
- package/windows/start-dsh-crew.ps1 +49 -5
package/src/client/index.tsx
CHANGED
|
@@ -52,7 +52,7 @@ const COPY = {
|
|
|
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: '视觉与生图',
|
|
55
|
+
sectionNames: { integrations: 'Codex / Claude / ZCode 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', harnessProviders: 'Harness Providers', providers: '多模态适配器', jobs: '任务状态' },
|
|
56
56
|
openHarness: '打开 3210 Crew Harness →',
|
|
57
57
|
harnessHint: '底层 Provider、Harness Models 与运行时配置',
|
|
58
58
|
hostReadiness: '宿主集成就绪度', hostReadinessHint: '只使用结构化安装与运行时证据;缺少证据不会显示 READY。',
|
|
@@ -75,11 +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}`,
|
|
82
|
+
noPriority: '未配置优先项', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
|
|
83
|
+
healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `${s} 秒前检查`,
|
|
83
84
|
partialCatalog: '部分 Provider 读取失败', catalogFailed: '无法读取 Harness 模型目录', removePriority: '移除', moveUp: '上移', moveDown: '下移',
|
|
84
85
|
needsModelSelection: '多个 Provider 提供默认偏好模型,且 Harness Default 无法消歧;当前将回退到 Harness Default,请手动选择。',
|
|
85
86
|
responsibilities: '职责',
|
|
@@ -100,9 +101,17 @@ const COPY = {
|
|
|
100
101
|
tierPolicyDesc: { auto: 'auto · orchestrator 自选', 'flash-only': '只用 flash', 'pro-only': '只用 pro' },
|
|
101
102
|
escalateHint: 'flash 失败自动用 pro 重试一次',
|
|
102
103
|
presetFlash: 'flash 模式', presetPro: 'pro 模式',
|
|
103
|
-
multimodal: '多模态',
|
|
104
|
-
visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
|
|
105
|
-
|
|
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',
|
|
106
115
|
noCustomProviders: '暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。',
|
|
107
116
|
providerName: '名称', modelsField: '模型列表(逗号分隔)',
|
|
108
117
|
providerType: '接入方式', typeApi: 'API · 兼容 OpenAI 接口', typeCli: 'CLI · 本地命令',
|
|
@@ -131,7 +140,11 @@ const COPY = {
|
|
|
131
140
|
roleWorker: 'Worker 角色(执行)', roleReviewer: 'Reviewer 角色(独立审查)',
|
|
132
141
|
workerStateLabel: 'Worker 状态', reviewStateLabel: 'Reviewer 状态',
|
|
133
142
|
roleStateHint: 'auto:orchestrator 可自动派发;manual:仅显式点名;disabled:禁用。未显式设置时由协作模式推导。',
|
|
134
|
-
autoReview: '自动复审', autoReviewHint: 'worker 成功后自动运行一次 reviewer(独立模型策略,只读)',
|
|
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 · 不自动复审' },
|
|
135
148
|
isolation: '工作区隔离', isolationHint: 'worktree 为默认;非 Git 仓库时需显式 shared,否则任务以 NOT_GIT_REPOSITORY 失败。',
|
|
136
149
|
isolationDesc: { worktree: 'worktree · 每个 coding worker 独立临时 git worktree(主工作区不动)', shared: 'shared · 旧版就地执行' },
|
|
137
150
|
maxParallel: '最大并行', maxParallelHint: '同时运行的工作流上限(1–16);超出进入队列。',
|
|
@@ -140,7 +153,7 @@ const COPY = {
|
|
|
140
153
|
cardRuntime: { t: '执行与连接', d: 'worker 会话跑在本实例内还是独立进程,单个任务的超时上限,以及 CC / Codex 探测本实例的地址。' },
|
|
141
154
|
cardMM: { t: '视觉与生图', d: '给纯文本的 DSH 模型借来眼睛和画笔:describe_image、会话贴图转写与 generate_image 都用这里的设置。' },
|
|
142
155
|
cardCustomProv: { t: '自定义 Provider', d: '接入自己的 API 或本地命令;保存后会出现在上面的视觉 / 生图 provider 选择里。' },
|
|
143
|
-
modeDesc: { auto: 'auto · 优先
|
|
156
|
+
modeDesc: { auto: 'auto · 优先 3210', hub: 'hub · 必须 3210' },
|
|
144
157
|
save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
|
|
145
158
|
adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
|
|
146
159
|
adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
|
|
@@ -178,7 +191,7 @@ const COPY = {
|
|
|
178
191
|
expandAll: 'Expand all', collapseAll: 'Collapse all',
|
|
179
192
|
modelCount: (count: number) => `${count} models`, providerCount: (count: number) => `${count} providers`,
|
|
180
193
|
jobCount: (count: number) => `${count} jobs`, runningCount: (count: number) => `${count} running`,
|
|
181
|
-
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',
|
|
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' },
|
|
182
195
|
openHarness: 'Open 3210 Crew Harness →',
|
|
183
196
|
harnessHint: 'Low-level providers, Harness Models, and runtime configuration',
|
|
184
197
|
hostReadiness: 'Host integration readiness', hostReadinessHint: 'Uses structured installer and runtime evidence only; missing evidence is never READY.',
|
|
@@ -201,11 +214,12 @@ const COPY = {
|
|
|
201
214
|
statePresetHint: 'Switch to Custom to configure each tier state separately.',
|
|
202
215
|
workerProvider: 'Worker Provider',
|
|
203
216
|
workerProviderDesc: { 'follow-dsh': 'Follow Harness · use each tier\'s ordered model priority', 'deepseek-official': 'DeepSeek Official · use legacy fixed model routing' },
|
|
204
|
-
workerProviderHint: 'Applies to Hub workers only; Standalone
|
|
217
|
+
workerProviderHint: 'Applies to Hub workers only; Standalone is legacy migration metadata only and is not a production execution path.',
|
|
205
218
|
workerModels: 'Worker Models', refreshHarnessModels: 'Refresh Harness Models', refreshingModels: 'Refreshing…',
|
|
206
219
|
modelPriority: 'Model Priority', addPriorityModel: '+ Add Priority Model', modelSearch: 'Search Provider or Model…',
|
|
207
220
|
harnessDefault: 'Harness Default', providerUnavailable: 'Provider unavailable', notAdvertised: 'Currently not advertised',
|
|
208
|
-
noPriority: 'No priority models configured', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
|
|
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`,
|
|
209
223
|
partialCatalog: 'Some providers failed to load', catalogFailed: 'Unable to read Harness model catalog', removePriority: 'Remove', moveUp: 'Move up', moveDown: 'Move down',
|
|
210
224
|
needsModelSelection: 'Multiple providers advertise the preferred model and Harness Default cannot disambiguate them. Falling back to Harness Default; choose a priority model explicitly.',
|
|
211
225
|
responsibilities: 'Responsibilities',
|
|
@@ -226,9 +240,17 @@ const COPY = {
|
|
|
226
240
|
tierPolicyDesc: { auto: 'auto · orchestrator picks', 'flash-only': 'flash only', 'pro-only': 'pro only' },
|
|
227
241
|
escalateHint: 'retry a failed flash run once on pro',
|
|
228
242
|
presetFlash: 'flash preset', presetPro: 'pro preset',
|
|
229
|
-
multimodal: 'Multimodal',
|
|
230
|
-
visionProvider: 'Vision provider', visionModel: 'Vision model', imagegenProvider: 'Image-gen provider',
|
|
231
|
-
|
|
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',
|
|
232
254
|
noCustomProviders: 'None yet. Added providers appear in the vision / image-gen selects above.',
|
|
233
255
|
providerName: 'Name', modelsField: 'Models (comma-separated)',
|
|
234
256
|
providerType: 'Connection', typeApi: 'API · OpenAI-compatible', typeCli: 'CLI · local command',
|
|
@@ -257,7 +279,11 @@ const COPY = {
|
|
|
257
279
|
roleWorker: 'Worker role (execution)', roleReviewer: 'Reviewer role (independent review)',
|
|
258
280
|
workerStateLabel: 'Worker state', reviewStateLabel: 'Reviewer state',
|
|
259
281
|
roleStateHint: 'auto = the orchestrator may dispatch automatically; manual = only when explicitly named; disabled = off. When unset, derived from the collaboration mode.',
|
|
260
|
-
autoReview: 'Automatic review', autoReviewHint: 'Run one reviewer pass (independent model policy, read-only) after a successful worker run',
|
|
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' },
|
|
261
287
|
isolation: 'Workspace isolation', isolationHint: 'worktree is the default; non-git workspaces require explicit shared, otherwise jobs fail with NOT_GIT_REPOSITORY.',
|
|
262
288
|
isolationDesc: { worktree: 'worktree · each coding worker runs in its own temporary git worktree (primary untouched)', shared: 'shared · legacy in-place execution' },
|
|
263
289
|
maxParallel: 'Max parallel', maxParallelHint: 'Upper bound of concurrently running workflows (1–16); extra ones queue.',
|
|
@@ -266,7 +292,7 @@ const COPY = {
|
|
|
266
292
|
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.' },
|
|
267
293
|
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." },
|
|
268
294
|
cardCustomProv: { t: 'Custom providers', d: 'Bring your own API or local command; saved providers appear in the vision / image-gen selects above.' },
|
|
269
|
-
modeDesc: { auto: 'auto · prefer
|
|
295
|
+
modeDesc: { auto: 'auto · prefer 3210', hub: 'hub · require 3210' },
|
|
270
296
|
save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
|
|
271
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.',
|
|
272
298
|
adaptiveEnabled: 'Enable adaptive routing', adaptiveWindow: 'Health window', adaptiveSamples: 'Minimum samples', adaptiveBoundary: 'Effective for the next workflow',
|
|
@@ -499,15 +525,23 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
499
525
|
vision_command: string; imagegen_command: string;
|
|
500
526
|
} | null>(null);
|
|
501
527
|
const [modelAdd, setModelAdd] = useState<string | null>(null);
|
|
502
|
-
const [modelCatalog, setModelCatalog] = useState<any>(null);
|
|
503
|
-
const [modelCatalogError, setModelCatalogError] = useState('');
|
|
504
|
-
const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
|
|
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);
|
|
505
538
|
const [modelPickerTier, setModelPickerTier] = useState<'flash' | 'pro' | null>(null);
|
|
506
539
|
const [modelQuery, setModelQuery] = useState('');
|
|
507
540
|
const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
|
|
508
541
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
|
|
509
542
|
const [surface, setSurface] = useState<string>('detecting');
|
|
510
543
|
const [runtimeInfo, setRuntimeInfo] = useState<any>(undefined);
|
|
544
|
+
const [readinessSnapshot, setReadinessSnapshot] = useState<any>(undefined);
|
|
511
545
|
|
|
512
546
|
const toggleSection = (sectionId: string) => {
|
|
513
547
|
setExpandedSections((current) => ({ ...current, [sectionId]: !current[sectionId] }));
|
|
@@ -518,9 +552,13 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
518
552
|
writeSectionState(sectionStorage(), expandedSections);
|
|
519
553
|
}, [expandedSections]);
|
|
520
554
|
|
|
521
|
-
useEffect(() => {
|
|
522
|
-
if (modelCatalogError) setExpandedSections((current) => openSections(current, ['workflow', 'flash', 'pro']));
|
|
523
|
-
}, [modelCatalogError]);
|
|
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]);
|
|
524
562
|
|
|
525
563
|
useEffect(() => {
|
|
526
564
|
if (testResult && !testResult.busy && testResult.ok === false) {
|
|
@@ -579,25 +617,37 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
579
617
|
|
|
580
618
|
const refreshAll = useCallback(async () => {
|
|
581
619
|
try {
|
|
582
|
-
const [j, s, c, pr] = await Promise.all([
|
|
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
|
+
]);
|
|
583
624
|
if (j.ok) setJobs(j.jobs ?? []);
|
|
584
625
|
if (s.ok) setStatus(s.status);
|
|
585
626
|
if (c.ok) setConfig((prev: any) => prev ?? c.config);
|
|
586
|
-
if (pr.ok) setPresetOptions([
|
|
627
|
+
if (pr.ok) setPresetOptions([
|
|
587
628
|
{ value: 'default', label: copy.presetDefault(pr.defaultId) },
|
|
588
629
|
...(pr.presets ?? []).map((x: any) => ({ value: x.id, label: x.name ?? x.id })),
|
|
589
|
-
]);
|
|
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);
|
|
590
635
|
} catch { /* instance restarting */ }
|
|
591
636
|
}, [get]);
|
|
592
637
|
|
|
593
638
|
useEffect(() => {
|
|
594
639
|
if (surface !== CREW_UI_SURFACES.OFFICIAL) return undefined;
|
|
595
640
|
void refreshAll();
|
|
596
|
-
const timer = setInterval(() => {
|
|
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);
|
|
597
647
|
return () => clearInterval(timer);
|
|
598
648
|
}, [surface, refreshAll, get]);
|
|
599
649
|
|
|
600
|
-
const refreshHarnessModels = useCallback(async () => {
|
|
650
|
+
const refreshHarnessModels = useCallback(async () => {
|
|
601
651
|
setModelCatalogBusy(true);
|
|
602
652
|
setModelCatalogError('');
|
|
603
653
|
try {
|
|
@@ -609,11 +659,53 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
609
659
|
} finally {
|
|
610
660
|
setModelCatalogBusy(false);
|
|
611
661
|
}
|
|
612
|
-
}, [get, copy]);
|
|
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]);
|
|
613
701
|
|
|
614
702
|
useEffect(() => {
|
|
615
703
|
if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
|
|
616
704
|
}, [surface, refreshHarnessModels]);
|
|
705
|
+
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshProviderInventory();
|
|
708
|
+
}, [surface, refreshProviderInventory]);
|
|
617
709
|
|
|
618
710
|
const act = useCallback(async (target: string, confirmName?: string) => {
|
|
619
711
|
if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
|
|
@@ -648,8 +740,25 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
648
740
|
setConfig((c: any) => ({ ...c, [key]: value }));
|
|
649
741
|
void applyPatch({ [key]: value });
|
|
650
742
|
};
|
|
651
|
-
/** Text/number inputs: track locally, apply on blur. */
|
|
652
|
-
const fieldLocal = (key: string, value: any) => setConfig((c: any) => ({ ...c, [key]: value }));
|
|
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
|
+
};
|
|
653
762
|
|
|
654
763
|
// --- custom providers & user-added models -------------------------------
|
|
655
764
|
const customProviders: any[] = config?.custom_providers ?? [];
|
|
@@ -757,16 +866,93 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
757
866
|
</div>
|
|
758
867
|
);
|
|
759
868
|
};
|
|
760
|
-
const deleteProvider = (id: string) => {
|
|
869
|
+
const deleteProvider = (id: string) => {
|
|
761
870
|
if (!config) return;
|
|
762
871
|
const patch: any = { custom_providers: customProviders.filter((p) => p.id !== id) };
|
|
763
872
|
if (config.vision_provider === id) { patch.vision_provider = 'claude-code'; patch.vision_model = 'haiku'; }
|
|
764
873
|
if (config.imagegen_provider === id) patch.imagegen_provider = 'codex';
|
|
765
874
|
setConfig((c: any) => ({ ...c, ...patch }));
|
|
766
875
|
void applyPatch(patch);
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
|
|
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. */
|
|
770
956
|
const block = (text: { t: string; d: string }, fields: any, gridded = true) => (
|
|
771
957
|
<div style={S.block}>
|
|
772
958
|
<div>
|
|
@@ -793,22 +979,23 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
793
979
|
const adaptive = normalizeAdaptive(config?.worker?.model_policy?.adaptive);
|
|
794
980
|
const setAdaptiveLocal = (candidate: AdaptiveConfig) => {
|
|
795
981
|
const next = normalizeAdaptive(candidate);
|
|
982
|
+
const ordering = next.enabled ? 'health-aware' : 'manual';
|
|
796
983
|
setConfig((current: any) => ({
|
|
797
984
|
...current,
|
|
798
985
|
worker: {
|
|
799
986
|
...current.worker,
|
|
800
|
-
model_policy: { ...current.worker?.model_policy, adaptive: next },
|
|
987
|
+
model_policy: { ...current.worker?.model_policy, adaptive: next, ordering },
|
|
801
988
|
},
|
|
802
989
|
}));
|
|
803
|
-
return next;
|
|
990
|
+
return { next, ordering };
|
|
804
991
|
};
|
|
805
992
|
const saveAdaptive = (candidate: AdaptiveConfig) => {
|
|
806
|
-
const next = setAdaptiveLocal(candidate);
|
|
807
|
-
void applyPatch({ worker: { model_policy: { adaptive: next } } });
|
|
993
|
+
const { next, ordering } = setAdaptiveLocal(candidate);
|
|
994
|
+
void applyPatch({ worker: { model_policy: { adaptive: next, ordering } } });
|
|
808
995
|
};
|
|
809
996
|
const modelActivity = aggregateModelInvocations(jobs);
|
|
810
997
|
const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
|
|
811
|
-
const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface });
|
|
998
|
+
const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface, readinessSnapshot });
|
|
812
999
|
|
|
813
1000
|
if (!currentSurfaceResponsibilities.fullControlPlane) {
|
|
814
1001
|
return <MinimalCrewPanel locale={locale} surface={surface} runtime={runtimeInfo} />;
|
|
@@ -903,11 +1090,16 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
903
1090
|
setConfig((current: any) => ({ ...current, [key]: list, [`${tier}_model_priority_configured`]: true }));
|
|
904
1091
|
void applyPatch({ [key]: list });
|
|
905
1092
|
};
|
|
906
|
-
const modelMeta = (ref: any) => {
|
|
1093
|
+
const modelMeta = (ref: any) => {
|
|
907
1094
|
const provider = catalogProviders.find((item) => item.id === ref.provider);
|
|
908
1095
|
const model = provider?.models?.find((item: any) => item.id === ref.model);
|
|
909
|
-
return { provider, model };
|
|
910
|
-
};
|
|
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
|
+
: '';
|
|
911
1103
|
const filteredProviders = catalogProviders.map((provider) => ({
|
|
912
1104
|
...provider,
|
|
913
1105
|
models: (provider.models ?? []).filter((model: any) => {
|
|
@@ -929,15 +1121,19 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
929
1121
|
{priority.length === 0 && <div style={{ fontSize: 12, opacity: 0.55, marginBottom: 6 }}>{copy.noPriority}</div>}
|
|
930
1122
|
{ambiguousPreferred && <div style={{ fontSize: 11.5, color: '#c98735', marginBottom: 6 }}>{copy.needsModelSelection}</div>}
|
|
931
1123
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
|
932
|
-
{priority.map((ref, index) => {
|
|
933
|
-
const meta = modelMeta(ref);
|
|
934
|
-
const
|
|
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 : '';
|
|
935
1128
|
return (
|
|
936
1129
|
<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 }}>
|
|
937
1130
|
<span style={{ opacity: 0.55, width: 18 }}>{index + 1}.</span>
|
|
938
1131
|
<span style={{ flex: 1, minWidth: 0 }}>
|
|
939
1132
|
<span style={{ display: 'block', fontSize: 12.5 }}>{meta.model?.name ?? ref.model}</span>
|
|
940
|
-
<span style={{ display: 'block', fontSize: 10.5, opacity: 0.58, fontFamily: 'monospace' }}>{ref.provider} / {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>
|
|
941
1137
|
{warning && <span style={{ display: 'block', fontSize: 10.5, color: '#c98735' }}>{warning}</span>}
|
|
942
1138
|
</span>
|
|
943
1139
|
<button type="button" style={{ ...S.btn, padding: '2px 7px' }} title={copy.moveUp} disabled={index === 0}
|
|
@@ -1029,12 +1225,31 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1029
1225
|
<CustomSelect value={config.review_state ?? 'auto'} onChange={(v) => field('review_state', v)}
|
|
1030
1226
|
options={(['auto', 'manual', 'disabled'] as const).map((s) => ({ value: s, label: copy.tierStateDesc[s] }))} /></label>
|
|
1031
1227
|
</>))}
|
|
1032
|
-
{block({ t: copy.autoReview, d: copy.autoReviewHint }, (
|
|
1033
|
-
|
|
1034
|
-
<
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
+
</>))}
|
|
1038
1253
|
{block({ t: copy.isolation, d: copy.isolationHint }, (<>
|
|
1039
1254
|
<label style={S.field}><span style={S.fieldLabel}>{copy.isolation}</span>
|
|
1040
1255
|
<CustomSelect value={config.isolation ?? 'worktree'} onChange={(v) => field('isolation', v)}
|
|
@@ -1142,7 +1357,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1142
1357
|
{block(copy.cardRuntime, (<>
|
|
1143
1358
|
<label style={S.field}><span style={S.fieldLabel}>{copy.mode}</span>
|
|
1144
1359
|
<CustomSelect value={config.mode} onChange={(v) => field('mode', v)}
|
|
1145
|
-
options={(['auto', 'hub'
|
|
1360
|
+
options={(['auto', 'hub'] as const).map((m) => ({ value: m, label: copy.modeDesc[m] }))} /></label>
|
|
1146
1361
|
<label style={S.field}><span style={S.fieldLabel}>{copy.timeout}</span>
|
|
1147
1362
|
<input style={{ ...S.input, width: '100%', boxSizing: 'border-box' as const }} type="number" min={60} max={7200} value={config.default_timeout_seconds}
|
|
1148
1363
|
onChange={(e) => fieldLocal('default_timeout_seconds', Number(e.target.value))}
|
|
@@ -1227,7 +1442,69 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1227
1442
|
</>))}
|
|
1228
1443
|
</CollapsibleSection>
|
|
1229
1444
|
|
|
1230
|
-
<CollapsibleSection sectionId="
|
|
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}
|
|
1231
1508
|
summary={sectionSummary(copy.providerCount(customProviders.length), testResult?.ok === false && copy.testFail)}
|
|
1232
1509
|
expanded={!!expandedSections.providers} onToggle={() => toggleSection('providers')}>
|
|
1233
1510
|
{block(copy.cardCustomProv, (<>
|