@ran-sh/dsh-crew 0.3.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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +8 -0
  3. package/.mcp.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.de.md +359 -0
  6. package/README.es.md +359 -0
  7. package/README.fr.md +359 -0
  8. package/README.hi.md +359 -0
  9. package/README.id.md +359 -0
  10. package/README.ja.md +359 -0
  11. package/README.ko.md +359 -0
  12. package/README.md +360 -0
  13. package/README.pt.md +359 -0
  14. package/README.ru.md +359 -0
  15. package/README.th.md +359 -0
  16. package/README.tr.md +359 -0
  17. package/README.vi.md +359 -0
  18. package/README.zh-TW.md +359 -0
  19. package/README.zh.md +305 -0
  20. package/agents/ds-flash.md +26 -0
  21. package/agents/ds-pro.md +32 -0
  22. package/agents/ds-reviewer.md +23 -0
  23. package/agents/ds-worker.md +22 -0
  24. package/codex/agents/ds-flash.toml +30 -0
  25. package/codex/agents/ds-pro.toml +31 -0
  26. package/codex/agents/ds-reviewer.toml +28 -0
  27. package/codex/agents/ds-worker.toml +28 -0
  28. package/codex/prompts/dsh-config.md +3 -0
  29. package/codex/prompts/dsh-status.md +1 -0
  30. package/commands/config.md +11 -0
  31. package/commands/off.md +5 -0
  32. package/commands/on.md +5 -0
  33. package/commands/status.md +5 -0
  34. package/cordis.patch.yml +4 -0
  35. package/docs/images/dsh-crew-host.png +0 -0
  36. package/docs/images/dsh-crew-jobs.png +0 -0
  37. package/docs/images/dsh-crew-logo.png +0 -0
  38. package/docs/images/dsh-crew-overview.png +0 -0
  39. package/lib/client.js +2765 -0
  40. package/package.json +125 -0
  41. package/scripts/build-client.mjs +28 -0
  42. package/scripts/live-crew-smoke.mjs +39 -0
  43. package/scripts/live-policy-matrix.mjs +177 -0
  44. package/scripts/policy-probe.mjs +101 -0
  45. package/scripts/setup.mjs +294 -0
  46. package/scripts/smoke-real.mjs +110 -0
  47. package/scripts/smoke.mjs +78 -0
  48. package/scripts/verify-installer-fix.mjs +26 -0
  49. package/src/adaptive-routing.mjs +260 -0
  50. package/src/client/activation-summary.tsx +64 -0
  51. package/src/client/entry.tsx +236 -0
  52. package/src/client/index.tsx +1120 -0
  53. package/src/config-readiness.mjs +59 -0
  54. package/src/delivery.mjs +205 -0
  55. package/src/dsh-cli-runtime.mjs +251 -0
  56. package/src/failure-classification.mjs +172 -0
  57. package/src/hub/entry.mjs +98 -0
  58. package/src/hub/index.mjs +757 -0
  59. package/src/hub-client.mjs +132 -0
  60. package/src/hub-compatibility.mjs +49 -0
  61. package/src/i18n.mjs +19 -0
  62. package/src/install/cli.mjs +28 -0
  63. package/src/install/install-legacy.mjs +460 -0
  64. package/src/install/install.mjs +451 -0
  65. package/src/jobs.mjs +275 -0
  66. package/src/mcp-runtime.mjs +257 -0
  67. package/src/model-catalog.mjs +173 -0
  68. package/src/model-routing.mjs +391 -0
  69. package/src/multimodal.mjs +0 -0
  70. package/src/policy-legacy.mjs +830 -0
  71. package/src/policy.mjs +197 -0
  72. package/src/readiness-matrix.mjs +169 -0
  73. package/src/runtime-controls.mjs +90 -0
  74. package/src/runtime-identity.mjs +108 -0
  75. package/src/server.mjs +477 -0
  76. package/src/status-shard.mjs +52 -0
  77. package/src/structured-error-code.mjs +39 -0
  78. package/src/vision-route.mjs +138 -0
  79. package/src/workflow-runtime.mjs +567 -0
  80. package/src/workflow.mjs +160 -0
  81. package/src/workspace-audit.mjs +231 -0
  82. package/src/workspace-isolation.mjs +306 -0
  83. package/statusline/statusline.sh +14 -0
  84. package/statusline/worker-segment.sh +35 -0
  85. package/worker.cordis.yml +77 -0
package/lib/client.js ADDED
@@ -0,0 +1,2765 @@
1
+ window.__ModuleLoader__.load({ id: "@ran-sh/dsh-crew", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
+ let react = require("react");
5
+ let react_dom = require("react-dom");
6
+ let react_jsx_runtime = require("react/jsx-runtime");
7
+ //#region src/client/index.tsx
8
+ const inject = ["slots", "locale"];
9
+ const API = "/_dsh/dsh-crew";
10
+ const COPY = {
11
+ zh: {
12
+ label: "DSH Crew",
13
+ title: "DSH Crew",
14
+ intro: "把 Claude Code / Codex 的子任务派给本实例的 DSH agent,在宿主里显示为原生子代理、进度可见。配置派发默认值、执行方式,以及接入视觉与生图能力,一键装进宿主。",
15
+ integrations: "集成",
16
+ installed: "已安装",
17
+ notInstalled: "未安装",
18
+ hud: "HUD 段",
19
+ install: "安装",
20
+ update: "更新",
21
+ restore: "还原",
22
+ confirmRestore: (name) => `确定从 ${name} 移除 dsh-crew 集成?(settings 会先备份)`,
23
+ globalConfig: "全局配置",
24
+ globalHint: "修改即时保存到 ~/.config/dsh-crew/config.json;CC / Codex 的新会话自动读取为默认值(会话内可用 /dsh-crew:config 临时覆盖)。",
25
+ orchestration: "Agent 编排",
26
+ enableSubagents: "启用子 Agent",
27
+ enableSubagentsHint: "总开关。关闭后 dsh_run_worker / dsh_spawn_worker 在工具层拒绝派发(配置保留,重新开启即恢复)。",
28
+ subagentsKept: "配置已保留",
29
+ mainAgentMode: "主 Agent 模式",
30
+ mainAgentModeDesc: {
31
+ "direct-allowed": "直接允许 · 可自己做,也可委派",
32
+ "coordinator-first": "协调优先 · 先规划、拆分、委派,再检查验证",
33
+ "dispatcher-only": "仅派发 · 尽量只做规划、派发、审查与整合"
34
+ },
35
+ mainAgentModeHint: "这是宿主路由指引,不是对宿主自身工具的硬限制。",
36
+ collaborationMode: "协作模式",
37
+ collaborationModeDesc: {
38
+ "flash-only": "Flash Only · 只用 Flash",
39
+ "pro-only": "Pro Only · 只用 Pro",
40
+ balanced: "Balanced · 双 Auto",
41
+ "review-pipeline": "Review Pipeline · Flash 实现 + Pro 复审",
42
+ custom: "Custom · 完全手动"
43
+ },
44
+ tierCardFlash: "Flash",
45
+ tierCardPro: "Pro",
46
+ tierState: "状态",
47
+ tierStateDesc: {
48
+ disabled: "禁用",
49
+ manual: "手动(仅用户点名时可用)",
50
+ auto: "自动(orchestrator 可自动委派)"
51
+ },
52
+ stateManagedByPreset: "由协作模式管理",
53
+ statePresetHint: "切到 Custom 可单独配置各档状态。",
54
+ workerProvider: "Worker Provider",
55
+ workerProviderDesc: {
56
+ "follow-dsh": "跟随 Harness · 使用各档独立的模型优先级",
57
+ "deepseek-official": "DeepSeek Official · 使用旧版固定模型路由"
58
+ },
59
+ workerProviderHint: "仅作用于 Hub 模式;Standalone 始终用 deepseek-official + DEEPSEEK_API_KEY。",
60
+ workerModels: "Worker 模型",
61
+ refreshHarnessModels: "刷新 Harness 模型",
62
+ refreshingModels: "刷新中…",
63
+ modelPriority: "模型优先级",
64
+ addPriorityModel: "+ 添加优先模型",
65
+ modelSearch: "搜索 Provider 或 Model…",
66
+ harnessDefault: "Harness 默认模型",
67
+ providerUnavailable: "Provider 不可用",
68
+ notAdvertised: "当前未列出",
69
+ noPriority: "未配置优先项",
70
+ modelPoolSummary: (p, m) => `Providers: ${p} · Models: ${m}`,
71
+ partialCatalog: "部分 Provider 读取失败",
72
+ catalogFailed: "无法读取 Harness 模型目录",
73
+ removePriority: "移除",
74
+ moveUp: "上移",
75
+ moveDown: "下移",
76
+ needsModelSelection: "多个 Provider 提供默认偏好模型,且 Harness Default 无法消歧;当前将回退到 Harness Default,请手动选择。",
77
+ responsibilities: "职责",
78
+ responsibilitiesHint: "路由指引,仅用于描述分工;只剩一个 Auto 档时它承担全部可委派工作。",
79
+ roleLabels: {
80
+ implementation: "代码实现",
81
+ simple_fix: "简单修复",
82
+ tests: "测试",
83
+ search_inspection: "搜索 / 检查",
84
+ architecture: "架构",
85
+ complex_debugging: "复杂调试",
86
+ refactor: "重构",
87
+ code_review: "代码审查"
88
+ },
89
+ routing: "路由",
90
+ proReviewsFlash: "Pro 复审 Flash 改动",
91
+ proReviewsFlashHint: "Flash 成功后自动追加一次 Pro 复审(仅 blocking 派发)",
92
+ proReviewsLocked: "Review Pipeline 模式固定开启",
93
+ enableCrewVision: "启用 Crew 视觉",
94
+ enableCrewVisionHint: "关闭后不注册 Crew 的 describe_image 与视觉 route(可与你的 dsh-vision 等共存)。",
95
+ enableImagegen: "启用生图",
96
+ enableImagegenHint: "关闭后不注册 Crew 的 generate_image。",
97
+ capRestartHint: "该开关改变工具注册,需重启 DSH 生效",
98
+ capDisabledNote: "Crew 工具/route 已禁用(重启 DSH 后生效)",
99
+ tier: "默认档位",
100
+ effort: "默认推理",
101
+ mode: "执行模式",
102
+ timeout: "默认超时(秒)",
103
+ hubUrl: "Hub 地址",
104
+ tierPolicy: "档位策略",
105
+ escalate: "失败升档",
106
+ tierPolicyDesc: {
107
+ auto: "auto · orchestrator 自选",
108
+ "flash-only": "只用 flash",
109
+ "pro-only": "只用 pro"
110
+ },
111
+ escalateHint: "flash 失败自动用 pro 重试一次",
112
+ presetFlash: "flash 模式",
113
+ presetPro: "pro 模式",
114
+ multimodal: "多模态",
115
+ visionProvider: "视觉 provider",
116
+ visionModel: "视觉模型",
117
+ imagegenProvider: "生图 provider",
118
+ customProviders: "自定义 Provider",
119
+ addProvider: "+ 添加 Provider",
120
+ noCustomProviders: "暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。",
121
+ providerName: "名称",
122
+ modelsField: "模型列表(逗号分隔)",
123
+ providerType: "接入方式",
124
+ typeApi: "API · 兼容 OpenAI 接口",
125
+ typeCli: "CLI · 本地命令",
126
+ baseUrl: "Base URL",
127
+ apiKey: "API Key",
128
+ imagegenModel: "生图模型(留空=不提供生图)",
129
+ visionCmd: "视觉命令(可选)",
130
+ imagegenCmd: "生图命令(可选)",
131
+ apiFormHint: "走 OpenAI 兼容接口:视觉调 {base}/chat/completions(图片以 base64 内联),生图调 {base}/images/generations。Key 保存在本机 ~/.config/dsh-crew/config.json,不会外发到第三方。",
132
+ cliFormHint: "命令经 bash 执行,占位符会被安全引用后代入。视觉: {image} {question} {model},stdout 即答案;生图: {prompt} {output} {size},命令须把图片写到 {output}。两条命令至少填一条。",
133
+ saveProvider: "保存",
134
+ cancel: "取消",
135
+ edit: "编辑",
136
+ del: "删除",
137
+ testProvider: "连通测试",
138
+ testing: "测试中…",
139
+ testTip: "按当前填写的内容实测:API 会检查可达性与鉴权并真发一次视觉请求;CLI 会检查可执行文件并真跑一次视觉命令。生图只校验配置,不实际出图。",
140
+ testPass: "连通正常",
141
+ testFail: "连通失败",
142
+ staleHub: "本 DSH 实例还在跑旧版插件,缺少该接口 —— 重启 DSH 后再试",
143
+ emptyResponse: (path, status) => `${path} → HTTP ${status},响应为空`,
144
+ badJson: (path, body) => `${path} → 非 JSON 响应: ${body}`,
145
+ refreshModels: "重新从 CLI 获取模型列表",
146
+ presetDefault: (id) => `default · 跟随 DSH${id ? ` (${id})` : ""}`,
147
+ progressTip: (t, turn, step, calls) => `耗时 ${t} · 第${turn}轮第${step}步 · ${calls} 次工具调用`,
148
+ confirmDeleteProvider: (n) => `删除自定义 provider「${n}」?`,
149
+ needName: "⚠ 名称必填",
150
+ needCmd: "⚠ 视觉/生图命令至少填一条",
151
+ needBaseUrl: "⚠ Base URL 必填",
152
+ needModels: "⚠ 模型列表至少填一个",
153
+ keySet: "已配置",
154
+ keyPlaceholder: "sk-…(留空则不发送 Authorization)",
155
+ addModelTip: "添加模型到列表",
156
+ removeModelTip: "从列表移除当前模型",
157
+ modelPlaceholder: "模型 id,回车确认",
158
+ capVision: "视觉",
159
+ capImagegen: "生图",
160
+ groupDispatch: "派发",
161
+ groupRuntime: "运行",
162
+ groupMultimodal: "多模态",
163
+ groupWorkflow: "工作流 / 执行(v0.2)",
164
+ roleWorker: "Worker 角色(执行)",
165
+ roleReviewer: "Reviewer 角色(独立审查)",
166
+ workerStateLabel: "Worker 状态",
167
+ reviewStateLabel: "Reviewer 状态",
168
+ roleStateHint: "auto:orchestrator 可自动派发;manual:仅显式点名;disabled:禁用。未显式设置时由协作模式推导。",
169
+ autoReview: "自动复审",
170
+ autoReviewHint: "worker 成功后自动运行一次 reviewer(独立模型策略,只读)",
171
+ isolation: "工作区隔离",
172
+ isolationHint: "worktree 为默认;非 Git 仓库时需显式 shared,否则任务以 NOT_GIT_REPOSITORY 失败。",
173
+ isolationDesc: {
174
+ worktree: "worktree · 每个 coding worker 独立临时 git worktree(主工作区不动)",
175
+ shared: "shared · 旧版就地执行"
176
+ },
177
+ maxParallel: "最大并行",
178
+ maxParallelHint: "同时运行的工作流上限(1–16);超出进入队列。",
179
+ legacyCompatibility: "↓ 以下为兼容配置(legacy,Flash/Pro 仍可编辑)",
180
+ cardDispatch: {
181
+ t: "派发策略",
182
+ d: "orchestrator 未指定时的档位与推理强度,以及档位约束、失败升档与两档各自挂载的 Agent 预设。"
183
+ },
184
+ cardRuntime: {
185
+ t: "执行与连接",
186
+ d: "worker 会话跑在本实例内还是独立进程,单个任务的超时上限,以及 CC / Codex 探测本实例的地址。"
187
+ },
188
+ cardMM: {
189
+ t: "视觉与生图",
190
+ d: "给纯文本的 DSH 模型借来眼睛和画笔:describe_image、会话贴图转写与 generate_image 都用这里的设置。"
191
+ },
192
+ cardCustomProv: {
193
+ t: "自定义 Provider",
194
+ d: "接入自己的 API 或本地命令;保存后会出现在上面的视觉 / 生图 provider 选择里。"
195
+ },
196
+ modeDesc: {
197
+ auto: "auto · 优先 hub",
198
+ hub: "hub · 必须 hub",
199
+ standalone: "standalone · 独立进程"
200
+ },
201
+ save: "保存",
202
+ saved: "已保存",
203
+ jobs: "Worker 任务",
204
+ empty: "当前没有 worker 任务。",
205
+ col: {
206
+ id: "任务",
207
+ source: "来源",
208
+ tier: "档位",
209
+ status: "状态",
210
+ progress: "进度",
211
+ tokens: "tokens ⇅",
212
+ task: "内容"
213
+ },
214
+ working: "处理中…",
215
+ tips: {
216
+ claude: {
217
+ install: "一键装进 Claude Code:注册本地 marketplace、claude plugin install、写入工具权限白名单、接线 claude-hud worker 段;修改 settings.json 前自动备份",
218
+ update: "重跑安装流程:刷新注册与权限、迁移旧格式配置、更新 HUD 接线;幂等,可放心点",
219
+ restore: "从 Claude Code 移除集成:清除 marketplace 注册、插件启用项与权限规则(settings 先备份);不删除插件源码与已跑任务"
220
+ },
221
+ codex: {
222
+ install: "把 ds-flash / ds-pro 角色和 /dsh-config、/dsh-status 命令复制到 ~/.codex/(MCP 路径按本机自动渲染,含 approve 审批与超时配置)",
223
+ update: "重新渲染并覆盖角色文件与命令(插件路径或配置变更后用);原文件先备份",
224
+ restore: "删除 ~/.codex/ 下的 ds-flash / ds-pro 角色与 dsh 命令文件(角色文件先备份)"
225
+ }
226
+ }
227
+ },
228
+ en: {
229
+ label: "DSH Crew",
230
+ title: "DSH Crew",
231
+ intro: "Dispatch Claude Code / Codex subtasks to DSH agents within this instance, displayed as native subagents in the host with live progress. Configure dispatch defaults, execution mode, and vision/image generation (via subscription CLI or your own OpenAI API), then one-click install into the host.",
232
+ integrations: "Integrations",
233
+ installed: "installed",
234
+ notInstalled: "not installed",
235
+ hud: "HUD segment",
236
+ install: "Install",
237
+ update: "Update",
238
+ restore: "Restore",
239
+ confirmRestore: (name) => `Remove the dsh-crew integration from ${name}? (settings are backed up first)`,
240
+ globalConfig: "Global configuration",
241
+ 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).",
242
+ orchestration: "Agent orchestration",
243
+ enableSubagents: "Enable Subagents",
244
+ enableSubagentsHint: "Master switch. When off, dsh_run_worker / dsh_spawn_worker refuse dispatch at the tool layer (the configuration is kept; re-enable to restore).",
245
+ subagentsKept: "Configuration kept",
246
+ mainAgentMode: "Main Agent Mode",
247
+ mainAgentModeDesc: {
248
+ "direct-allowed": "Direct Allowed · implement directly or delegate",
249
+ "coordinator-first": "Coordinator First · plan, decompose, delegate, verify",
250
+ "dispatcher-only": "Dispatcher Only · plan, dispatch, review, integrate"
251
+ },
252
+ mainAgentModeHint: "Host routing guidance — not a hard restriction on the host's own tools.",
253
+ collaborationMode: "Collaboration Mode",
254
+ collaborationModeDesc: {
255
+ "flash-only": "Flash Only",
256
+ "pro-only": "Pro Only",
257
+ balanced: "Balanced",
258
+ "review-pipeline": "Review Pipeline · implement + review",
259
+ custom: "Custom"
260
+ },
261
+ tierCardFlash: "Flash",
262
+ tierCardPro: "Pro",
263
+ tierState: "State",
264
+ tierStateDesc: {
265
+ disabled: "Disabled",
266
+ manual: "Manual (only when the user names this tier)",
267
+ auto: "Auto (the orchestrator may delegate to it)"
268
+ },
269
+ stateManagedByPreset: "managed by the collaboration mode",
270
+ statePresetHint: "Switch to Custom to configure each tier state separately.",
271
+ workerProvider: "Worker Provider",
272
+ workerProviderDesc: {
273
+ "follow-dsh": "Follow Harness · use each tier's ordered model priority",
274
+ "deepseek-official": "DeepSeek Official · use legacy fixed model routing"
275
+ },
276
+ workerProviderHint: "Applies to Hub workers only; Standalone always uses deepseek-official + DEEPSEEK_API_KEY.",
277
+ workerModels: "Worker Models",
278
+ refreshHarnessModels: "Refresh Harness Models",
279
+ refreshingModels: "Refreshing…",
280
+ modelPriority: "Model Priority",
281
+ addPriorityModel: "+ Add Priority Model",
282
+ modelSearch: "Search Provider or Model…",
283
+ harnessDefault: "Harness Default",
284
+ providerUnavailable: "Provider unavailable",
285
+ notAdvertised: "Currently not advertised",
286
+ noPriority: "No priority models configured",
287
+ modelPoolSummary: (p, m) => `Providers: ${p} · Models: ${m}`,
288
+ partialCatalog: "Some providers failed to load",
289
+ catalogFailed: "Unable to read Harness model catalog",
290
+ removePriority: "Remove",
291
+ moveUp: "Move up",
292
+ moveDown: "Move down",
293
+ needsModelSelection: "Multiple providers advertise the preferred model and Harness Default cannot disambiguate them. Falling back to Harness Default; choose a priority model explicitly.",
294
+ responsibilities: "Responsibilities",
295
+ responsibilitiesHint: "Routing guidance describing the split of work; with a single Auto tier left, that tier carries all delegatable coding work.",
296
+ roleLabels: {
297
+ implementation: "Code implementation",
298
+ simple_fix: "Simple fixes",
299
+ tests: "Tests",
300
+ search_inspection: "Search / inspection",
301
+ architecture: "Architecture",
302
+ complex_debugging: "Complex debugging",
303
+ refactor: "Refactoring",
304
+ code_review: "Code review"
305
+ },
306
+ routing: "Routing",
307
+ proReviewsFlash: "Pro reviews Flash changes",
308
+ proReviewsFlashHint: "Automatically follow a successful Flash run with one Pro review (blocking dispatch only)",
309
+ proReviewsLocked: "always on in Review Pipeline mode",
310
+ enableCrewVision: "Enable Crew Vision",
311
+ enableCrewVisionHint: "When off, the Crew describe_image tool and vision route are not registered (coexist with your own dsh-vision or other plugins).",
312
+ enableImagegen: "Enable Image Generation",
313
+ enableImagegenHint: "When off, the Crew generate_image tool is not registered.",
314
+ capRestartHint: "This switch changes tool registration and takes effect after a DSH restart",
315
+ capDisabledNote: "Crew tool/route disabled (effective after DSH restart)",
316
+ tier: "Default tier",
317
+ effort: "Default effort",
318
+ mode: "Mode",
319
+ timeout: "Timeout (s)",
320
+ hubUrl: "Hub URL",
321
+ tierPolicy: "Tier policy",
322
+ escalate: "Escalate on failure",
323
+ tierPolicyDesc: {
324
+ auto: "auto · orchestrator picks",
325
+ "flash-only": "flash only",
326
+ "pro-only": "pro only"
327
+ },
328
+ escalateHint: "retry a failed flash run once on pro",
329
+ presetFlash: "flash preset",
330
+ presetPro: "pro preset",
331
+ multimodal: "Multimodal",
332
+ visionProvider: "Vision provider",
333
+ visionModel: "Vision model",
334
+ imagegenProvider: "Image-gen provider",
335
+ customProviders: "Custom providers",
336
+ addProvider: "+ Add provider",
337
+ noCustomProviders: "None yet. Added providers appear in the vision / image-gen selects above.",
338
+ providerName: "Name",
339
+ modelsField: "Models (comma-separated)",
340
+ providerType: "Connection",
341
+ typeApi: "API · OpenAI-compatible",
342
+ typeCli: "CLI · local command",
343
+ baseUrl: "Base URL",
344
+ apiKey: "API key",
345
+ imagegenModel: "Image-gen model (empty = no image generation)",
346
+ visionCmd: "Vision command (optional)",
347
+ imagegenCmd: "Image-gen command (optional)",
348
+ apiFormHint: "OpenAI-compatible: vision calls {base}/chat/completions (image inlined as base64), image-gen calls {base}/images/generations. The key is stored locally in ~/.config/dsh-crew/config.json and never sent to third parties.",
349
+ cliFormHint: "Commands run via bash with shell-quoted placeholders. Vision: {image} {question} {model}, stdout is the answer; image-gen: {prompt} {output} {size}, the command must write the image to {output}. Fill in at least one command.",
350
+ saveProvider: "Save",
351
+ cancel: "Cancel",
352
+ edit: "Edit",
353
+ del: "Delete",
354
+ testProvider: "Test",
355
+ testing: "Testing…",
356
+ testTip: "Probes what is currently filled in: API checks reachability and auth then makes one real vision call; CLI checks the executable then runs the vision command once. Image generation is only validated, never actually run.",
357
+ testPass: "Connection OK",
358
+ testFail: "Connection failed",
359
+ staleHub: "This DSH instance is still running an older build of the plugin and lacks this route — restart DSH and retry",
360
+ emptyResponse: (path, status) => `${path} → HTTP ${status}, empty response`,
361
+ badJson: (path, body) => `${path} → non-JSON response: ${body}`,
362
+ refreshModels: "Re-fetch the model list from the CLI",
363
+ presetDefault: (id) => `default · follow DSH${id ? ` (${id})` : ""}`,
364
+ progressTip: (t, turn, step, calls) => `${t} elapsed · turn ${turn}, step ${step} · ${calls} tool calls`,
365
+ confirmDeleteProvider: (n) => `Delete custom provider "${n}"?`,
366
+ needName: "⚠ Name is required",
367
+ needCmd: "⚠ Fill in at least one command",
368
+ needBaseUrl: "⚠ Base URL is required",
369
+ needModels: "⚠ Add at least one model",
370
+ keySet: "set",
371
+ keyPlaceholder: "sk-… (leave empty to send no Authorization header)",
372
+ addModelTip: "Add a model to the list",
373
+ removeModelTip: "Remove the current model from the list",
374
+ modelPlaceholder: "model id, Enter to confirm",
375
+ capVision: "vision",
376
+ capImagegen: "image-gen",
377
+ groupDispatch: "Dispatch",
378
+ groupRuntime: "Runtime",
379
+ groupMultimodal: "Multimodal",
380
+ groupWorkflow: "Workflow / Execution (v0.2)",
381
+ roleWorker: "Worker role (execution)",
382
+ roleReviewer: "Reviewer role (independent review)",
383
+ workerStateLabel: "Worker state",
384
+ reviewStateLabel: "Reviewer state",
385
+ roleStateHint: "auto = the orchestrator may dispatch automatically; manual = only when explicitly named; disabled = off. When unset, derived from the collaboration mode.",
386
+ autoReview: "Automatic review",
387
+ autoReviewHint: "Run one reviewer pass (independent model policy, read-only) after a successful worker run",
388
+ isolation: "Workspace isolation",
389
+ isolationHint: "worktree is the default; non-git workspaces require explicit shared, otherwise jobs fail with NOT_GIT_REPOSITORY.",
390
+ isolationDesc: {
391
+ worktree: "worktree · each coding worker runs in its own temporary git worktree (primary untouched)",
392
+ shared: "shared · legacy in-place execution"
393
+ },
394
+ maxParallel: "Max parallel",
395
+ maxParallelHint: "Upper bound of concurrently running workflows (1–16); extra ones queue.",
396
+ legacyCompatibility: "↓ Legacy compatibility controls below (Flash/Pro still editable)",
397
+ cardDispatch: {
398
+ t: "Dispatch policy",
399
+ d: "Tier and effort used when the orchestrator names none, plus tier clamping, failure escalation and the Agent preset mounted per tier."
400
+ },
401
+ cardRuntime: {
402
+ t: "Execution & connection",
403
+ d: "Whether worker sessions run inside this instance or a separate process, the per-job timeout, and the address CC / Codex probes."
404
+ },
405
+ cardMM: {
406
+ t: "Vision & image generation",
407
+ d: "Lends the harness's text-only models eyes and a brush: describe_image, pasted-image transcription and generate_image all use these settings."
408
+ },
409
+ cardCustomProv: {
410
+ t: "Custom providers",
411
+ d: "Bring your own API or local command; saved providers appear in the vision / image-gen selects above."
412
+ },
413
+ modeDesc: {
414
+ auto: "auto · prefer hub",
415
+ hub: "hub · require hub",
416
+ standalone: "standalone"
417
+ },
418
+ save: "Save",
419
+ saved: "Saved",
420
+ jobs: "Worker jobs",
421
+ empty: "No worker jobs yet.",
422
+ col: {
423
+ id: "job",
424
+ source: "source",
425
+ tier: "tier",
426
+ status: "status",
427
+ progress: "progress",
428
+ tokens: "tokens ⇅",
429
+ task: "task"
430
+ },
431
+ working: "Working…",
432
+ tips: {
433
+ claude: {
434
+ install: "One-click install into Claude Code: register local marketplace, claude plugin install, permission allowlist, claude-hud segment wiring; settings.json backed up first",
435
+ update: "Re-run the installer: refresh registration & permissions, migrate legacy config, update HUD wiring; idempotent",
436
+ restore: "Remove the integration from Claude Code: marketplace registration, plugin enablement and permission rules (settings backed up); plugin source and past jobs untouched"
437
+ },
438
+ codex: {
439
+ 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)",
440
+ update: "Re-render and overwrite role files and prompts (after path/config changes); originals backed up",
441
+ restore: "Delete ds-flash / ds-pro roles and dsh prompts from ~/.codex/ (roles backed up first)"
442
+ }
443
+ }
444
+ }
445
+ };
446
+ const S = {
447
+ section: {
448
+ margin: "8px 0 -2px",
449
+ fontSize: 13,
450
+ fontWeight: 600,
451
+ opacity: .9
452
+ },
453
+ group: {
454
+ margin: "4px 0 -3px",
455
+ fontSize: 12,
456
+ opacity: .55
457
+ },
458
+ block: {
459
+ border: "1px solid rgba(128,128,128,0.22)",
460
+ borderRadius: 10,
461
+ padding: "11px 14px 13px",
462
+ display: "flex",
463
+ flexDirection: "column",
464
+ gap: 10
465
+ },
466
+ blockGrid: {
467
+ display: "grid",
468
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
469
+ gap: "12px 14px",
470
+ alignItems: "end"
471
+ },
472
+ settingTitle: {
473
+ fontWeight: 600,
474
+ fontSize: 13
475
+ },
476
+ settingDesc: {
477
+ fontSize: 11.5,
478
+ opacity: .6,
479
+ marginTop: 2,
480
+ lineHeight: 1.5
481
+ },
482
+ card: {
483
+ border: "1px solid rgba(128,128,128,0.22)",
484
+ borderRadius: 8,
485
+ padding: "10px 14px",
486
+ display: "flex",
487
+ alignItems: "center",
488
+ gap: 10,
489
+ flexWrap: "wrap"
490
+ },
491
+ grid: {
492
+ border: "1px solid rgba(128,128,128,0.22)",
493
+ borderRadius: 8,
494
+ padding: "12px 14px",
495
+ display: "grid",
496
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
497
+ gap: "12px 14px",
498
+ alignItems: "end"
499
+ },
500
+ btn: {
501
+ padding: "4px 12px",
502
+ borderRadius: 6,
503
+ border: "1px solid rgba(128,128,128,0.35)",
504
+ background: "transparent",
505
+ color: "inherit",
506
+ cursor: "pointer",
507
+ fontSize: 12.5
508
+ },
509
+ chip: (on) => ({
510
+ fontSize: 11.5,
511
+ padding: "1px 8px",
512
+ borderRadius: 99,
513
+ border: "1px solid",
514
+ borderColor: on ? "rgba(63,185,80,0.5)" : "rgba(128,128,128,0.35)",
515
+ color: on ? "#3fb950" : "inherit",
516
+ opacity: on ? 1 : .55
517
+ }),
518
+ field: {
519
+ display: "flex",
520
+ flexDirection: "column",
521
+ gap: 3
522
+ },
523
+ fieldLabel: {
524
+ fontSize: 11,
525
+ opacity: .6
526
+ },
527
+ input: {
528
+ padding: "3px 8px",
529
+ borderRadius: 5,
530
+ border: "1px solid rgba(128,128,128,0.35)",
531
+ background: "transparent",
532
+ color: "inherit",
533
+ fontSize: 12.5
534
+ },
535
+ cell: {
536
+ padding: "5px 10px 5px 0",
537
+ borderBottom: "1px solid rgba(128,128,128,0.16)",
538
+ textAlign: "left",
539
+ verticalAlign: "top"
540
+ },
541
+ tight: {
542
+ padding: "5px 8px 5px 0",
543
+ borderBottom: "1px solid rgba(128,128,128,0.16)",
544
+ textAlign: "left",
545
+ verticalAlign: "top",
546
+ whiteSpace: "nowrap",
547
+ overflow: "hidden",
548
+ textOverflow: "ellipsis"
549
+ },
550
+ mono: {
551
+ fontFamily: "ui-monospace, Menlo, monospace",
552
+ fontSize: 12,
553
+ fontVariantNumeric: "tabular-nums"
554
+ }
555
+ };
556
+ const VISION_MODELS = {
557
+ "claude-code": [
558
+ "haiku",
559
+ "sonnet",
560
+ "opus"
561
+ ],
562
+ codex: [
563
+ "default",
564
+ "gpt-5.4-mini",
565
+ "gpt-5.6"
566
+ ],
567
+ grok: ["default"],
568
+ agy: ["default"]
569
+ };
570
+ function CustomSelect({ value, options, onChange, minWidth }) {
571
+ const [open, setOpen] = (0, react.useState)(null);
572
+ (0, react.useEffect)(() => {
573
+ if (!open) return;
574
+ const close = () => setOpen(null);
575
+ const onKey = (e) => {
576
+ if (e.key === "Escape") close();
577
+ };
578
+ const t = setTimeout(() => document.addEventListener("mousedown", close), 0);
579
+ document.addEventListener("keydown", onKey);
580
+ return () => {
581
+ clearTimeout(t);
582
+ document.removeEventListener("mousedown", close);
583
+ document.removeEventListener("keydown", onKey);
584
+ };
585
+ }, [open]);
586
+ const current = options.find((o) => o.value === value);
587
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
588
+ type: "button",
589
+ style: {
590
+ ...S.input,
591
+ display: "flex",
592
+ alignItems: "center",
593
+ gap: 6,
594
+ cursor: "pointer",
595
+ minWidth: minWidth ?? 92,
596
+ width: "100%",
597
+ boxSizing: "border-box",
598
+ justifyContent: "space-between"
599
+ },
600
+ onClick: (e) => {
601
+ const r = e.currentTarget.getBoundingClientRect();
602
+ const up = window.innerHeight - r.bottom < 240;
603
+ setOpen({
604
+ left: r.left,
605
+ top: r.bottom + 4,
606
+ bottom: window.innerHeight - r.top + 4,
607
+ width: Math.max(r.width, 130),
608
+ up
609
+ });
610
+ },
611
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
612
+ style: {
613
+ overflow: "hidden",
614
+ textOverflow: "ellipsis",
615
+ whiteSpace: "nowrap"
616
+ },
617
+ children: current?.label ?? value
618
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
619
+ style: {
620
+ opacity: .5,
621
+ fontSize: 10
622
+ },
623
+ children: "▾"
624
+ })]
625
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
626
+ style: {
627
+ position: "fixed",
628
+ left: open.left,
629
+ ...open.up ? { bottom: open.bottom } : { top: open.top },
630
+ minWidth: open.width,
631
+ zIndex: 9999,
632
+ background: "Canvas",
633
+ color: "CanvasText",
634
+ border: "1px solid rgba(128,128,128,0.3)",
635
+ borderRadius: 8,
636
+ boxShadow: "0 8px 28px rgba(0,0,0,0.22)",
637
+ padding: 4,
638
+ maxHeight: 240,
639
+ overflowY: "auto"
640
+ },
641
+ onMouseDown: (e) => e.stopPropagation(),
642
+ children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
643
+ onClick: () => {
644
+ onChange(o.value);
645
+ setOpen(null);
646
+ },
647
+ style: {
648
+ padding: "5px 10px",
649
+ borderRadius: 5,
650
+ cursor: "pointer",
651
+ fontSize: 12.5,
652
+ whiteSpace: "nowrap",
653
+ background: o.value === value ? "rgba(128,128,128,0.16)" : "transparent",
654
+ display: "flex",
655
+ justifyContent: "space-between",
656
+ gap: 12,
657
+ alignItems: "center"
658
+ },
659
+ onMouseEnter: (e) => {
660
+ e.currentTarget.style.background = "rgba(128,128,128,0.24)";
661
+ },
662
+ onMouseLeave: (e) => {
663
+ e.currentTarget.style.background = o.value === value ? "rgba(128,128,128,0.16)" : "transparent";
664
+ },
665
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: o.label ?? o.value }), o.value === value && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
666
+ style: {
667
+ opacity: .6,
668
+ fontSize: 11
669
+ },
670
+ children: "✓"
671
+ })]
672
+ }, o.value))
673
+ }), document.body)] });
674
+ }
675
+ function sourceLabel(source) {
676
+ if (source === "claude-code") return "CC";
677
+ if (source === "codex") return "Codex";
678
+ if (source === "api" || source === void 0) return "API";
679
+ return source;
680
+ }
681
+ function elapsed(startedAt, endedAt) {
682
+ const s = Math.max(0, Math.round(((endedAt ? +new Date(endedAt) : Date.now()) - +new Date(startedAt)) / 1e3));
683
+ return s >= 60 ? `${Math.floor(s / 60)}m${s % 60}s` : `${s}s`;
684
+ }
685
+ function ktok(n) {
686
+ return n >= 1e3 ? `${Math.round(n / 100) / 10}k` : `${n}`;
687
+ }
688
+ function WorkersPanel({ ctx }) {
689
+ const locale = (0, react.useSyncExternalStore)((notify) => ctx.on("locale/change", notify), () => ctx.locale.getLocale().active, () => ctx.locale.getLocale().active);
690
+ const copy = COPY[locale === "zh" ? "zh" : "en"];
691
+ const [jobs, setJobs] = (0, react.useState)([]);
692
+ const [hoverTask, setHoverTask] = (0, react.useState)(null);
693
+ const [status, setStatus] = (0, react.useState)(null);
694
+ const [config, setConfig] = (0, react.useState)(null);
695
+ const [dynModels, setDynModels] = (0, react.useState)({});
696
+ const [presetOptions, setPresetOptions] = (0, react.useState)([{ value: "default" }]);
697
+ const [notice, setNotice] = (0, react.useState)("");
698
+ const [busy, setBusy] = (0, react.useState)(false);
699
+ const [provForm, setProvForm] = (0, react.useState)(null);
700
+ const [modelAdd, setModelAdd] = (0, react.useState)(null);
701
+ const [modelCatalog, setModelCatalog] = (0, react.useState)(null);
702
+ const [modelCatalogError, setModelCatalogError] = (0, react.useState)("");
703
+ const [modelCatalogBusy, setModelCatalogBusy] = (0, react.useState)(false);
704
+ const [modelPickerTier, setModelPickerTier] = (0, react.useState)(null);
705
+ const [modelQuery, setModelQuery] = (0, react.useState)("");
706
+ const [testResult, setTestResult] = (0, react.useState)(null);
707
+ /**
708
+ * The panel is served from disk on every load, but the hub's routes are
709
+ * registered at instance boot — after an upgrade the new UI can call a route
710
+ * the running instance does not have yet, which answers 404/405 with an empty
711
+ * body. Report that as "restart DSH" instead of a JSON parse error.
712
+ */
713
+ const readJson = (0, react.useCallback)(async (res, path) => {
714
+ const text = await res.text();
715
+ if (text === "") throw new Error(res.status === 404 || res.status === 405 ? `${copy.staleHub}(${path} → HTTP ${res.status})` : copy.emptyResponse(path, res.status));
716
+ try {
717
+ return JSON.parse(text);
718
+ } catch {
719
+ throw new Error(copy.badJson(path, text.slice(0, 160)));
720
+ }
721
+ }, [copy]);
722
+ /** Every request carries the active locale: the panel is the authority on it
723
+ * (DSH's setting may be unset), and the hub localizes its own strings from it. */
724
+ const withLang = (0, react.useCallback)((path) => `${API}${path}${path.includes("?") ? "&" : "?"}lang=${locale === "zh" ? "zh" : "en"}`, [locale]);
725
+ const get = (0, react.useCallback)(async (path) => readJson(await fetch(withLang(path), { cache: "no-store" }), path), [readJson, withLang]);
726
+ const post = (0, react.useCallback)(async (path, body) => readJson(await fetch(withLang(path), {
727
+ method: "POST",
728
+ headers: { "content-type": "application/json" },
729
+ body: JSON.stringify({
730
+ ...body,
731
+ lang: locale === "zh" ? "zh" : "en"
732
+ })
733
+ }), path), [
734
+ readJson,
735
+ withLang,
736
+ locale
737
+ ]);
738
+ const refreshAll = (0, react.useCallback)(async () => {
739
+ try {
740
+ const [j, s, c, pr] = await Promise.all([
741
+ get("/jobs"),
742
+ get("/install/status"),
743
+ get("/config"),
744
+ get("/presets")
745
+ ]);
746
+ if (j.ok) setJobs(j.jobs ?? []);
747
+ if (s.ok) setStatus(s.status);
748
+ if (c.ok) setConfig((prev) => prev ?? c.config);
749
+ if (pr.ok) setPresetOptions([{
750
+ value: "default",
751
+ label: copy.presetDefault(pr.defaultId)
752
+ }, ...(pr.presets ?? []).map((x) => ({
753
+ value: x.id,
754
+ label: x.name ?? x.id
755
+ }))]);
756
+ } catch {}
757
+ }, [get]);
758
+ (0, react.useEffect)(() => {
759
+ refreshAll();
760
+ const timer = setInterval(() => {
761
+ get("/jobs").then((j) => {
762
+ if (j.ok) setJobs(j.jobs ?? []);
763
+ }).catch(() => {});
764
+ }, 3e3);
765
+ return () => clearInterval(timer);
766
+ }, [refreshAll, get]);
767
+ const refreshHarnessModels = (0, react.useCallback)(async () => {
768
+ setModelCatalogBusy(true);
769
+ setModelCatalogError("");
770
+ try {
771
+ const result = await get("/models");
772
+ if (!result.ok) throw new Error(result.error ?? copy.catalogFailed);
773
+ setModelCatalog(result);
774
+ } catch (error) {
775
+ setModelCatalogError(error?.message ?? copy.catalogFailed);
776
+ } finally {
777
+ setModelCatalogBusy(false);
778
+ }
779
+ }, [get, copy]);
780
+ (0, react.useEffect)(() => {
781
+ refreshHarnessModels();
782
+ }, [refreshHarnessModels]);
783
+ const act = (0, react.useCallback)(async (target, confirmName) => {
784
+ if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
785
+ setBusy(true);
786
+ setNotice(copy.working);
787
+ try {
788
+ const r = await post("/install", { target });
789
+ if (r.ok === false) throw new Error(r.error);
790
+ setNotice((r.actions ?? []).join(";"));
791
+ const s = await get("/install/status");
792
+ if (s.ok) setStatus(s.status);
793
+ } catch (e) {
794
+ setNotice(String(e?.message ?? e));
795
+ } finally {
796
+ setBusy(false);
797
+ }
798
+ }, [
799
+ post,
800
+ get,
801
+ copy
802
+ ]);
803
+ const applyPatch = (0, react.useCallback)((patch) => {
804
+ post("/config", patch).then((r) => {
805
+ if (r.ok) {
806
+ setConfig(r.config);
807
+ setNotice(copy.saved);
808
+ setTimeout(() => setNotice(""), 1500);
809
+ }
810
+ }).catch(() => {});
811
+ }, [post, copy]);
812
+ /** Selects & checkboxes: apply immediately. */
813
+ const field = (key, value) => {
814
+ setConfig((c) => ({
815
+ ...c,
816
+ [key]: value
817
+ }));
818
+ applyPatch({ [key]: value });
819
+ };
820
+ /** Text/number inputs: track locally, apply on blur. */
821
+ const fieldLocal = (key, value) => setConfig((c) => ({
822
+ ...c,
823
+ [key]: value
824
+ }));
825
+ const customProviders = config?.custom_providers ?? [];
826
+ const extraModels = config?.extra_models ?? {};
827
+ const RESERVED_IDS = [
828
+ "claude-code",
829
+ "codex",
830
+ "grok",
831
+ "agy",
832
+ "off",
833
+ "custom",
834
+ "default"
835
+ ];
836
+ const provType = (p) => p.type ?? (p.vision_command || p.imagegen_command ? "cli" : "api");
837
+ const canSee = (p) => provType(p) === "cli" ? !!p.vision_command : !!p.base_url && (p.models?.length ?? 0) > 0;
838
+ const canDraw = (p) => provType(p) === "cli" ? !!p.imagegen_command : !!p.base_url && !!String(p.imagegen_model ?? "").trim();
839
+ const visionProviderOptions = [
840
+ { value: "claude-code" },
841
+ { value: "codex" },
842
+ { value: "grok" },
843
+ { value: "agy" },
844
+ ...customProviders.filter(canSee).map((p) => ({
845
+ value: p.id,
846
+ label: p.name || p.id
847
+ })),
848
+ { value: "off" }
849
+ ];
850
+ const imagegenProviderOptions = [
851
+ { value: "codex" },
852
+ {
853
+ value: "agy",
854
+ label: "agy (nano banana)"
855
+ },
856
+ {
857
+ value: "grok",
858
+ label: "grok (imagine)"
859
+ },
860
+ ...customProviders.filter(canDraw).map((p) => ({
861
+ value: p.id,
862
+ label: p.name || p.id
863
+ })),
864
+ { value: "off" }
865
+ ];
866
+ const visionModelOptions = (() => {
867
+ if (!config) return [];
868
+ const p = config.vision_provider;
869
+ const custom = customProviders.find((c) => c.id === p);
870
+ const base = custom ? [{ value: "default" }, ...(custom.models ?? []).map((m) => ({ value: m }))] : dynModels[p] ?? (VISION_MODELS[p] ?? ["default"]).map((m) => ({ value: m }));
871
+ const extras = (extraModels[p] ?? []).filter((m) => !base.some((o) => o.value === m)).map((m) => ({ value: m }));
872
+ return [...base, ...extras];
873
+ })();
874
+ const commitModelAdd = () => {
875
+ const m = (modelAdd ?? "").trim();
876
+ setModelAdd(null);
877
+ if (m === "" || !config) return;
878
+ const p = config.vision_provider;
879
+ const cur = extraModels[p] ?? [];
880
+ const nextExtra = {
881
+ ...extraModels,
882
+ [p]: cur.includes(m) ? cur : [...cur, m]
883
+ };
884
+ setConfig((c) => ({
885
+ ...c,
886
+ extra_models: nextExtra,
887
+ vision_model: m
888
+ }));
889
+ applyPatch({
890
+ extra_models: nextExtra,
891
+ vision_model: m
892
+ });
893
+ };
894
+ const removeCurrentModel = () => {
895
+ if (!config) return;
896
+ const p = config.vision_provider;
897
+ const m = config.vision_model;
898
+ const nextExtra = {
899
+ ...extraModels,
900
+ [p]: (extraModels[p] ?? []).filter((x) => x !== m)
901
+ };
902
+ const fallback = visionModelOptions.find((o) => o.value !== m)?.value ?? "default";
903
+ setConfig((c) => ({
904
+ ...c,
905
+ extra_models: nextExtra,
906
+ vision_model: fallback
907
+ }));
908
+ applyPatch({
909
+ extra_models: nextExtra,
910
+ vision_model: fallback
911
+ });
912
+ };
913
+ const saveProviderForm = () => {
914
+ if (!provForm || !config) return;
915
+ const name = provForm.name.trim();
916
+ if (name === "") {
917
+ setNotice(copy.needName);
918
+ return;
919
+ }
920
+ const models = provForm.models.split(/[,,\n]+/).map((s) => s.trim()).filter(Boolean);
921
+ const vision_command = provForm.vision_command.trim();
922
+ const imagegen_command = provForm.imagegen_command.trim();
923
+ if (provForm.type === "cli") {
924
+ if (vision_command === "" && imagegen_command === "") {
925
+ setNotice(copy.needCmd);
926
+ return;
927
+ }
928
+ } else {
929
+ if (provForm.base_url.trim() === "") {
930
+ setNotice(copy.needBaseUrl);
931
+ return;
932
+ }
933
+ if (models.length === 0) {
934
+ setNotice(copy.needModels);
935
+ return;
936
+ }
937
+ }
938
+ let id = provForm.originalId;
939
+ if (!id) {
940
+ const base = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "provider";
941
+ id = base;
942
+ let n = 2;
943
+ while (RESERVED_IDS.includes(id) || customProviders.some((p) => p.id === id)) id = `${base}-${n++}`;
944
+ }
945
+ const entry = provForm.type === "api" ? {
946
+ id,
947
+ name,
948
+ type: "api",
949
+ models,
950
+ base_url: provForm.base_url.trim(),
951
+ api_key: provForm.api_key,
952
+ imagegen_model: provForm.imagegen_model.trim()
953
+ } : {
954
+ id,
955
+ name,
956
+ type: "cli",
957
+ models,
958
+ vision_command,
959
+ imagegen_command
960
+ };
961
+ const next = provForm.originalId ? customProviders.map((p) => p.id === provForm.originalId ? entry : p) : [...customProviders, entry];
962
+ field("custom_providers", next);
963
+ setProvForm(null);
964
+ };
965
+ const runProviderTest = (key, entry) => {
966
+ setTestResult({
967
+ key,
968
+ busy: true
969
+ });
970
+ post("/provider-test", entry).then((r) => setTestResult(r.ok ? {
971
+ key,
972
+ busy: false,
973
+ ok: r.result.ok,
974
+ steps: r.result.steps
975
+ } : {
976
+ key,
977
+ busy: false,
978
+ ok: false,
979
+ error: r.error
980
+ })).catch((e) => setTestResult({
981
+ key,
982
+ busy: false,
983
+ ok: false,
984
+ error: String(e?.message ?? e)
985
+ }));
986
+ };
987
+ /** Shared renderer for a test outcome under the button that triggered it. */
988
+ const testReport = (key) => {
989
+ if (!testResult || testResult.key !== key) return null;
990
+ if (testResult.busy) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
991
+ style: {
992
+ fontSize: 11.5,
993
+ opacity: .6
994
+ },
995
+ children: copy.testing
996
+ });
997
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
998
+ style: {
999
+ fontSize: 11.5,
1000
+ display: "flex",
1001
+ flexDirection: "column",
1002
+ gap: 2
1003
+ },
1004
+ children: [
1005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1006
+ style: { color: testResult.ok ? "#3fb950" : "#f85149" },
1007
+ children: testResult.ok ? `✓ ${copy.testPass}` : `✗ ${copy.testFail}`
1008
+ }),
1009
+ testResult.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1010
+ style: {
1011
+ opacity: .7,
1012
+ wordBreak: "break-all"
1013
+ },
1014
+ children: testResult.error
1015
+ }),
1016
+ (testResult.steps ?? []).map((st, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1017
+ style: {
1018
+ opacity: .7,
1019
+ wordBreak: "break-all"
1020
+ },
1021
+ children: [
1022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1023
+ style: { color: st.ok ? "#3fb950" : "#f85149" },
1024
+ children: st.ok ? "✓" : "✗"
1025
+ }),
1026
+ " ",
1027
+ st.name,
1028
+ ": ",
1029
+ st.detail
1030
+ ]
1031
+ }, i))
1032
+ ]
1033
+ });
1034
+ };
1035
+ const deleteProvider = (id) => {
1036
+ if (!config) return;
1037
+ const patch = { custom_providers: customProviders.filter((p) => p.id !== id) };
1038
+ if (config.vision_provider === id) {
1039
+ patch.vision_provider = "claude-code";
1040
+ patch.vision_model = "haiku";
1041
+ }
1042
+ if (config.imagegen_provider === id) patch.imagegen_provider = "codex";
1043
+ setConfig((c) => ({
1044
+ ...c,
1045
+ ...patch
1046
+ }));
1047
+ applyPatch(patch);
1048
+ };
1049
+ /** Card: title + one-line description header, then its fields. */
1050
+ const block = (text, fields, gridded = true) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1051
+ style: S.block,
1052
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1053
+ style: S.settingTitle,
1054
+ children: text.t
1055
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1056
+ style: S.settingDesc,
1057
+ children: text.d
1058
+ })] }), gridded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1059
+ style: S.blockGrid,
1060
+ children: fields
1061
+ }) : fields]
1062
+ });
1063
+ const integrationRow = (name, installed, extra, installTarget, uninstallTarget, tips) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1064
+ style: S.card,
1065
+ children: [
1066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1067
+ style: {
1068
+ fontWeight: 600,
1069
+ fontSize: 13,
1070
+ minWidth: 92
1071
+ },
1072
+ children: name
1073
+ }),
1074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1075
+ style: S.chip(installed),
1076
+ children: installed ? `● ${copy.installed}` : `○ ${copy.notInstalled}`
1077
+ }),
1078
+ extra,
1079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
1080
+ !installed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1081
+ style: S.btn,
1082
+ title: tips.install,
1083
+ disabled: busy,
1084
+ onClick: () => {
1085
+ act(installTarget);
1086
+ },
1087
+ children: copy.install
1088
+ }),
1089
+ installed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1090
+ style: S.btn,
1091
+ title: tips.update,
1092
+ disabled: busy,
1093
+ onClick: () => {
1094
+ act(installTarget);
1095
+ },
1096
+ children: copy.update
1097
+ }),
1098
+ installed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1099
+ style: {
1100
+ ...S.btn,
1101
+ opacity: .75
1102
+ },
1103
+ title: tips.restore,
1104
+ disabled: busy,
1105
+ onClick: () => {
1106
+ act(uninstallTarget, name);
1107
+ },
1108
+ children: copy.restore
1109
+ })
1110
+ ]
1111
+ });
1112
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
+ style: {
1114
+ display: "flex",
1115
+ flexDirection: "column",
1116
+ gap: 8,
1117
+ fontSize: 13,
1118
+ lineHeight: 1.55
1119
+ },
1120
+ children: [
1121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1122
+ style: {
1123
+ fontSize: 19,
1124
+ fontWeight: 600,
1125
+ marginBottom: -2
1126
+ },
1127
+ children: copy.title
1128
+ }),
1129
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1130
+ style: { opacity: .75 },
1131
+ children: copy.intro
1132
+ }),
1133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1134
+ style: S.section,
1135
+ children: copy.integrations
1136
+ }),
1137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1138
+ style: {
1139
+ display: "flex",
1140
+ flexDirection: "column",
1141
+ gap: 8
1142
+ },
1143
+ children: [integrationRow("Claude Code", !!status?.claude?.installed, status?.claude?.installed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1144
+ style: S.chip(!!status?.claude?.hud),
1145
+ children: copy.hud
1146
+ }) : null, "claude", "claude-uninstall", copy.tips.claude), integrationRow("Codex", !!status?.codex?.installed, null, "codex", "codex-uninstall", copy.tips.codex)]
1147
+ }),
1148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1149
+ style: S.section,
1150
+ children: copy.globalConfig
1151
+ }),
1152
+ config && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1153
+ (() => {
1154
+ const clampNum = (v) => {
1155
+ const n = Number(v);
1156
+ if (Number.isNaN(n)) return 3;
1157
+ return Math.max(1, Math.min(16, n));
1158
+ };
1159
+ const mode = config.collaboration_mode ?? "flash-only";
1160
+ const nonCustom = mode !== "custom";
1161
+ const states = nonCustom ? {
1162
+ "flash-only": {
1163
+ flash: "auto",
1164
+ pro: "disabled"
1165
+ },
1166
+ "pro-only": {
1167
+ flash: "disabled",
1168
+ pro: "auto"
1169
+ },
1170
+ balanced: {
1171
+ flash: "auto",
1172
+ pro: "auto"
1173
+ },
1174
+ "review-pipeline": {
1175
+ flash: "auto",
1176
+ pro: "auto"
1177
+ },
1178
+ custom: null
1179
+ }[mode] : null;
1180
+ const effState = (tier) => states ? states[tier] : (tier === "flash" ? config.flash_state : config.pro_state) ?? "auto";
1181
+ const roleOptions = [
1182
+ "implementation",
1183
+ "simple_fix",
1184
+ "tests",
1185
+ "search_inspection",
1186
+ "architecture",
1187
+ "complex_debugging",
1188
+ "refactor",
1189
+ "code_review"
1190
+ ];
1191
+ const roleKeys = (tier) => {
1192
+ const arr = tier === "flash" ? config.flash_roles ?? [] : config.pro_roles ?? [];
1193
+ const set = new Set(arr);
1194
+ return {
1195
+ arr: roleOptions.filter((r) => set.has(r)),
1196
+ set,
1197
+ save: (list) => field(tier === "flash" ? "flash_roles" : "pro_roles", list)
1198
+ };
1199
+ };
1200
+ const catalogProviders = modelCatalog?.providers ?? [];
1201
+ const refKey = (ref) => `${ref?.provider ?? ""}\0${ref?.model ?? ""}`;
1202
+ const priorityFor = (tier) => Array.isArray(config[`${tier}_model_priority`]) ? config[`${tier}_model_priority`] : [];
1203
+ const savePriority = (tier, list) => {
1204
+ const key = `${tier}_model_priority`;
1205
+ setConfig((current) => ({
1206
+ ...current,
1207
+ [key]: list,
1208
+ [`${tier}_model_priority_configured`]: true
1209
+ }));
1210
+ applyPatch({ [key]: list });
1211
+ };
1212
+ const modelMeta = (ref) => {
1213
+ const provider = catalogProviders.find((item) => item.id === ref.provider);
1214
+ return {
1215
+ provider,
1216
+ model: provider?.models?.find((item) => item.id === ref.model)
1217
+ };
1218
+ };
1219
+ const filteredProviders = catalogProviders.map((provider) => ({
1220
+ ...provider,
1221
+ models: (provider.models ?? []).filter((model) => {
1222
+ const query = modelQuery.trim().toLowerCase();
1223
+ return !query || [
1224
+ provider.id,
1225
+ provider.name,
1226
+ model.id,
1227
+ model.name
1228
+ ].some((value) => String(value ?? "").toLowerCase().includes(query));
1229
+ })
1230
+ })).filter((provider) => provider.models.length > 0);
1231
+ const modelPriorityPanel = (tier) => {
1232
+ const priority = priorityFor(tier);
1233
+ const selected = new Set(priority.map(refKey));
1234
+ const preferredId = tier === "flash" ? "deepseek-v4-flash" : "deepseek-v4-pro";
1235
+ const preferredProviders = catalogProviders.filter((provider) => (provider.models ?? []).some((model) => model.id === preferredId));
1236
+ const ambiguousPreferred = priority.length === 0 && config[`${tier}_model_priority_configured`] !== true && preferredProviders.length > 1 && !preferredProviders.some((provider) => provider.id === modelCatalog?.harness_default?.provider);
1237
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1238
+ style: {
1239
+ gridColumn: "1 / -1",
1240
+ borderTop: "1px solid rgba(128,128,128,0.18)",
1241
+ paddingTop: 9
1242
+ },
1243
+ children: [
1244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1245
+ style: {
1246
+ ...S.fieldLabel,
1247
+ marginBottom: 5
1248
+ },
1249
+ children: copy.modelPriority
1250
+ }),
1251
+ priority.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1252
+ style: {
1253
+ fontSize: 12,
1254
+ opacity: .55,
1255
+ marginBottom: 6
1256
+ },
1257
+ children: copy.noPriority
1258
+ }),
1259
+ ambiguousPreferred && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1260
+ style: {
1261
+ fontSize: 11.5,
1262
+ color: "#c98735",
1263
+ marginBottom: 6
1264
+ },
1265
+ children: copy.needsModelSelection
1266
+ }),
1267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1268
+ style: {
1269
+ display: "flex",
1270
+ flexDirection: "column",
1271
+ gap: 5
1272
+ },
1273
+ children: priority.map((ref, index) => {
1274
+ const meta = modelMeta(ref);
1275
+ const warning = !meta.provider ? copy.providerUnavailable : !meta.model ? copy.notAdvertised : "";
1276
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1277
+ style: {
1278
+ display: "flex",
1279
+ gap: 7,
1280
+ alignItems: "center",
1281
+ padding: "6px 8px",
1282
+ border: "1px solid rgba(128,128,128,0.2)",
1283
+ borderRadius: 6
1284
+ },
1285
+ children: [
1286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1287
+ style: {
1288
+ opacity: .55,
1289
+ width: 18
1290
+ },
1291
+ children: [index + 1, "."]
1292
+ }),
1293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1294
+ style: {
1295
+ flex: 1,
1296
+ minWidth: 0
1297
+ },
1298
+ children: [
1299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1300
+ style: {
1301
+ display: "block",
1302
+ fontSize: 12.5
1303
+ },
1304
+ children: meta.model?.name ?? ref.model
1305
+ }),
1306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1307
+ style: {
1308
+ display: "block",
1309
+ fontSize: 10.5,
1310
+ opacity: .58,
1311
+ fontFamily: "monospace"
1312
+ },
1313
+ children: [
1314
+ ref.provider,
1315
+ " / ",
1316
+ ref.model
1317
+ ]
1318
+ }),
1319
+ warning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1320
+ style: {
1321
+ display: "block",
1322
+ fontSize: 10.5,
1323
+ color: "#c98735"
1324
+ },
1325
+ children: warning
1326
+ })
1327
+ ]
1328
+ }),
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1330
+ type: "button",
1331
+ style: {
1332
+ ...S.btn,
1333
+ padding: "2px 7px"
1334
+ },
1335
+ title: copy.moveUp,
1336
+ disabled: index === 0,
1337
+ onClick: () => {
1338
+ const next = [...priority];
1339
+ [next[index - 1], next[index]] = [next[index], next[index - 1]];
1340
+ savePriority(tier, next);
1341
+ },
1342
+ children: "↑"
1343
+ }),
1344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1345
+ type: "button",
1346
+ style: {
1347
+ ...S.btn,
1348
+ padding: "2px 7px"
1349
+ },
1350
+ title: copy.moveDown,
1351
+ disabled: index === priority.length - 1,
1352
+ onClick: () => {
1353
+ const next = [...priority];
1354
+ [next[index], next[index + 1]] = [next[index + 1], next[index]];
1355
+ savePriority(tier, next);
1356
+ },
1357
+ children: "↓"
1358
+ }),
1359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1360
+ type: "button",
1361
+ style: {
1362
+ ...S.btn,
1363
+ padding: "2px 7px"
1364
+ },
1365
+ title: copy.removePriority,
1366
+ onClick: () => savePriority(tier, priority.filter((_, itemIndex) => itemIndex !== index)),
1367
+ children: "×"
1368
+ })
1369
+ ]
1370
+ }, refKey(ref));
1371
+ })
1372
+ }),
1373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1374
+ style: {
1375
+ display: "flex",
1376
+ gap: 8,
1377
+ alignItems: "center",
1378
+ marginTop: 7
1379
+ },
1380
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1381
+ type: "button",
1382
+ style: S.btn,
1383
+ disabled: !modelCatalog || modelCatalogBusy,
1384
+ onClick: () => {
1385
+ setModelPickerTier(modelPickerTier === tier ? null : tier);
1386
+ setModelQuery("");
1387
+ },
1388
+ children: copy.addPriorityModel
1389
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1390
+ style: {
1391
+ fontSize: 11,
1392
+ opacity: .58
1393
+ },
1394
+ children: [
1395
+ copy.harnessDefault,
1396
+ ": ",
1397
+ modelCatalog?.harness_default ? `${modelCatalog.harness_default.provider} / ${modelCatalog.harness_default.model}` : "—"
1398
+ ]
1399
+ })]
1400
+ }),
1401
+ modelPickerTier === tier && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1402
+ style: {
1403
+ marginTop: 8,
1404
+ border: "1px solid rgba(128,128,128,0.22)",
1405
+ borderRadius: 7,
1406
+ padding: 8,
1407
+ maxHeight: 260,
1408
+ overflow: "auto"
1409
+ },
1410
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1411
+ autoFocus: true,
1412
+ value: modelQuery,
1413
+ placeholder: copy.modelSearch,
1414
+ onChange: (event) => setModelQuery(event.target.value),
1415
+ style: {
1416
+ ...S.input,
1417
+ width: "100%",
1418
+ boxSizing: "border-box",
1419
+ marginBottom: 7
1420
+ }
1421
+ }), filteredProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1422
+ style: { marginBottom: 8 },
1423
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1424
+ style: {
1425
+ fontSize: 11.5,
1426
+ fontWeight: 600,
1427
+ marginBottom: 3
1428
+ },
1429
+ children: [
1430
+ provider.name,
1431
+ " ",
1432
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1433
+ style: {
1434
+ opacity: .5,
1435
+ fontFamily: "monospace"
1436
+ },
1437
+ children: provider.id
1438
+ })
1439
+ ]
1440
+ }), (provider.models ?? []).map((model) => {
1441
+ const ref = {
1442
+ provider: provider.id,
1443
+ model: model.id
1444
+ };
1445
+ const duplicate = selected.has(refKey(ref));
1446
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1447
+ type: "button",
1448
+ disabled: duplicate,
1449
+ style: {
1450
+ ...S.btn,
1451
+ display: "block",
1452
+ width: "100%",
1453
+ textAlign: "left",
1454
+ marginBottom: 3,
1455
+ opacity: duplicate ? .45 : 1
1456
+ },
1457
+ onClick: () => {
1458
+ savePriority(tier, [...priority, ref]);
1459
+ setModelPickerTier(null);
1460
+ },
1461
+ children: [
1462
+ model.name,
1463
+ " ",
1464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1465
+ style: {
1466
+ opacity: .55,
1467
+ fontFamily: "monospace"
1468
+ },
1469
+ children: model.id
1470
+ })
1471
+ ]
1472
+ }, model.id);
1473
+ })]
1474
+ }, provider.id))]
1475
+ })
1476
+ ]
1477
+ });
1478
+ };
1479
+ const tierCard = (tier, card) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1480
+ style: S.block,
1481
+ children: [
1482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1483
+ style: S.settingTitle,
1484
+ children: card.t
1485
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1486
+ style: S.settingDesc,
1487
+ children: card.d
1488
+ })] }),
1489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1490
+ style: S.blockGrid,
1491
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1492
+ style: S.field,
1493
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1494
+ style: S.fieldLabel,
1495
+ children: copy.tierState
1496
+ }), nonCustom ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1497
+ style: {
1498
+ fontSize: 12.5,
1499
+ padding: "3px 8px",
1500
+ borderRadius: 5,
1501
+ border: "1px solid rgba(128,128,128,0.35)",
1502
+ opacity: .8
1503
+ },
1504
+ children: [
1505
+ effState(tier) === "auto" ? copy.tierStateDesc.auto : copy.tierStateDesc.disabled,
1506
+ "(",
1507
+ copy.stateManagedByPreset,
1508
+ ")"
1509
+ ]
1510
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1511
+ value: tier === "flash" ? config.flash_state ?? "auto" : config.pro_state ?? "auto",
1512
+ onChange: (v) => field(tier === "flash" ? "flash_state" : "pro_state", v),
1513
+ options: [
1514
+ "disabled",
1515
+ "manual",
1516
+ "auto"
1517
+ ].map((s) => ({
1518
+ value: s,
1519
+ label: copy.tierStateDesc[s]
1520
+ }))
1521
+ })]
1522
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1523
+ style: S.field,
1524
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1525
+ style: S.fieldLabel,
1526
+ children: tier === "flash" ? copy.presetFlash : copy.presetPro
1527
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1528
+ value: tier === "flash" ? config.preset_flash ?? "default" : config.preset_pro ?? "default",
1529
+ onChange: (v) => field(tier === "flash" ? "preset_flash" : "preset_pro", v),
1530
+ options: presetOptions
1531
+ })]
1532
+ })]
1533
+ }),
1534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1535
+ style: {
1536
+ fontSize: 11,
1537
+ opacity: .6,
1538
+ marginTop: 2
1539
+ },
1540
+ children: [
1541
+ copy.statePresetHint,
1542
+ " ",
1543
+ copy.responsibilitiesHint
1544
+ ]
1545
+ }),
1546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1547
+ style: {
1548
+ display: "grid",
1549
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
1550
+ gap: "3px 14px"
1551
+ },
1552
+ children: roleOptions.map((r) => {
1553
+ const k = roleKeys(tier);
1554
+ const on = k.set.has(r);
1555
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1556
+ style: {
1557
+ display: "flex",
1558
+ alignItems: "center",
1559
+ gap: 6,
1560
+ fontSize: 12
1561
+ },
1562
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1563
+ type: "checkbox",
1564
+ checked: on,
1565
+ onChange: () => {
1566
+ k.save(on ? k.arr.filter((x) => x !== r) : [...k.arr, r]);
1567
+ }
1568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: copy.roleLabels[r] })]
1569
+ }, r);
1570
+ })
1571
+ }),
1572
+ modelPriorityPanel(tier)
1573
+ ]
1574
+ });
1575
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1577
+ style: S.group,
1578
+ children: copy.groupWorkflow
1579
+ }),
1580
+ block({
1581
+ t: copy.roleWorker,
1582
+ d: copy.roleStateHint
1583
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1584
+ style: S.field,
1585
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1586
+ style: S.fieldLabel,
1587
+ children: copy.workerStateLabel
1588
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1589
+ value: config.worker_state ?? "auto",
1590
+ onChange: (v) => field("worker_state", v),
1591
+ options: [
1592
+ "auto",
1593
+ "manual",
1594
+ "disabled"
1595
+ ].map((s) => ({
1596
+ value: s,
1597
+ label: copy.tierStateDesc[s]
1598
+ }))
1599
+ })]
1600
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1601
+ style: S.field,
1602
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1603
+ style: S.fieldLabel,
1604
+ children: copy.reviewStateLabel
1605
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1606
+ value: config.review_state ?? "auto",
1607
+ onChange: (v) => field("review_state", v),
1608
+ options: [
1609
+ "auto",
1610
+ "manual",
1611
+ "disabled"
1612
+ ].map((s) => ({
1613
+ value: s,
1614
+ label: copy.tierStateDesc[s]
1615
+ }))
1616
+ })]
1617
+ })] })),
1618
+ block({
1619
+ t: copy.autoReview,
1620
+ d: copy.autoReviewHint
1621
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1622
+ style: {
1623
+ ...S.field,
1624
+ flexDirection: "row",
1625
+ alignItems: "center",
1626
+ gap: 6
1627
+ },
1628
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1629
+ type: "checkbox",
1630
+ checked: config.auto_review ?? !!config.pro_reviews_flash,
1631
+ onChange: (e) => field("auto_review", e.target.checked)
1632
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1633
+ style: { fontSize: 12.5 },
1634
+ children: copy.autoReview
1635
+ })]
1636
+ }) }), false),
1637
+ block({
1638
+ t: copy.isolation,
1639
+ d: copy.isolationHint
1640
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1641
+ style: S.field,
1642
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1643
+ style: S.fieldLabel,
1644
+ children: copy.isolation
1645
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1646
+ value: config.isolation ?? "worktree",
1647
+ onChange: (v) => field("isolation", v),
1648
+ options: ["worktree", "shared"].map((s) => ({
1649
+ value: s,
1650
+ label: copy.isolationDesc[s]
1651
+ }))
1652
+ })]
1653
+ }) })),
1654
+ block({
1655
+ t: copy.maxParallel,
1656
+ d: copy.maxParallelHint
1657
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1658
+ style: S.field,
1659
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1660
+ style: S.fieldLabel,
1661
+ children: copy.maxParallel
1662
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1663
+ type: "number",
1664
+ min: 1,
1665
+ max: 16,
1666
+ value: config.max_parallel ?? 3,
1667
+ onChange: (e) => fieldLocal("max_parallel", clampNum(e.target.value)),
1668
+ onBlur: () => field("max_parallel", clampNum(config.max_parallel)),
1669
+ style: S.input
1670
+ })]
1671
+ }) })),
1672
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1673
+ style: {
1674
+ fontSize: 11,
1675
+ opacity: .6,
1676
+ marginTop: 8,
1677
+ marginBottom: 2
1678
+ },
1679
+ children: copy.legacyCompatibility
1680
+ }),
1681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1682
+ style: S.group,
1683
+ children: copy.orchestration
1684
+ }),
1685
+ block({
1686
+ t: copy.orchestration,
1687
+ d: copy.enableSubagentsHint
1688
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1689
+ style: {
1690
+ ...S.field,
1691
+ flexDirection: "row",
1692
+ alignItems: "center",
1693
+ gap: 6
1694
+ },
1695
+ title: copy.enableSubagentsHint,
1696
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1697
+ type: "checkbox",
1698
+ checked: !!config.subagents_enabled,
1699
+ onChange: (e) => field("subagents_enabled", e.target.checked)
1700
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1701
+ style: { fontSize: 12.5 },
1702
+ children: config.subagents_enabled === false ? copy.subagentsKept : copy.enableSubagents
1703
+ })]
1704
+ }) }), false),
1705
+ block({
1706
+ t: copy.mainAgentMode,
1707
+ d: copy.mainAgentModeHint
1708
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1709
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1710
+ style: S.field,
1711
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1712
+ style: S.fieldLabel,
1713
+ children: copy.mainAgentMode
1714
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1715
+ value: config.main_agent_mode ?? "direct-allowed",
1716
+ onChange: (v) => field("main_agent_mode", v),
1717
+ options: [
1718
+ "direct-allowed",
1719
+ "coordinator-first",
1720
+ "dispatcher-only"
1721
+ ].map((m) => ({
1722
+ value: m,
1723
+ label: copy.mainAgentModeDesc[m]
1724
+ }))
1725
+ })]
1726
+ }),
1727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1728
+ style: S.field,
1729
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1730
+ style: S.fieldLabel,
1731
+ children: copy.collaborationMode
1732
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1733
+ value: mode,
1734
+ onChange: (v) => field("collaboration_mode", v),
1735
+ options: [
1736
+ "flash-only",
1737
+ "pro-only",
1738
+ "balanced",
1739
+ "review-pipeline",
1740
+ "custom"
1741
+ ].map((m) => ({
1742
+ value: m,
1743
+ label: copy.collaborationModeDesc[m]
1744
+ }))
1745
+ })]
1746
+ }),
1747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1748
+ style: S.field,
1749
+ title: copy.workerProviderHint,
1750
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1751
+ style: S.fieldLabel,
1752
+ children: copy.workerProvider
1753
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1754
+ value: config.worker_provider_mode ?? "follow-dsh",
1755
+ onChange: (v) => field("worker_provider_mode", v),
1756
+ options: ["follow-dsh", "deepseek-official"].map((m) => ({
1757
+ value: m,
1758
+ label: copy.workerProviderDesc[m]
1759
+ }))
1760
+ })]
1761
+ })
1762
+ ] })),
1763
+ block({
1764
+ t: copy.workerModels,
1765
+ d: modelCatalog ? copy.modelPoolSummary(modelCatalog.provider_count ?? 0, modelCatalog.model_count ?? 0) : copy.catalogFailed
1766
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1767
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1768
+ type: "button",
1769
+ style: S.btn,
1770
+ disabled: modelCatalogBusy,
1771
+ onClick: () => {
1772
+ refreshHarnessModels();
1773
+ },
1774
+ children: modelCatalogBusy ? copy.refreshingModels : copy.refreshHarnessModels
1775
+ }),
1776
+ modelCatalog?.partial && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1777
+ style: {
1778
+ fontSize: 11.5,
1779
+ color: "#c98735"
1780
+ },
1781
+ children: copy.partialCatalog
1782
+ }),
1783
+ modelCatalogError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1784
+ style: {
1785
+ fontSize: 11.5,
1786
+ color: "#c55"
1787
+ },
1788
+ children: modelCatalogError
1789
+ })
1790
+ ] }), false),
1791
+ tierCard("flash", {
1792
+ t: copy.tierCardFlash,
1793
+ d: "implementation · direct validation · Delivery Report"
1794
+ }),
1795
+ tierCard("pro", {
1796
+ t: copy.tierCardPro,
1797
+ d: "manual advanced work · review pipeline"
1798
+ }),
1799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1800
+ style: S.group,
1801
+ children: copy.routing
1802
+ }),
1803
+ block({
1804
+ t: copy.routing,
1805
+ d: ""
1806
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1807
+ style: {
1808
+ ...S.field,
1809
+ flexDirection: "row",
1810
+ alignItems: "center",
1811
+ gap: 6,
1812
+ paddingBottom: 5
1813
+ },
1814
+ title: copy.escalateHint,
1815
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1816
+ type: "checkbox",
1817
+ checked: !!config.escalate_on_failure,
1818
+ onChange: (e) => field("escalate_on_failure", e.target.checked)
1819
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1820
+ style: { fontSize: 12 },
1821
+ children: copy.escalate
1822
+ })]
1823
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1824
+ style: {
1825
+ ...S.field,
1826
+ flexDirection: "row",
1827
+ alignItems: "center",
1828
+ gap: 6,
1829
+ paddingBottom: 5
1830
+ },
1831
+ title: mode === "review-pipeline" ? copy.proReviewsLocked : copy.proReviewsFlashHint,
1832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1833
+ type: "checkbox",
1834
+ disabled: mode === "review-pipeline",
1835
+ checked: mode === "review-pipeline" ? true : !!config.pro_reviews_flash,
1836
+ onChange: (e) => field("pro_reviews_flash", e.target.checked)
1837
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1838
+ style: { fontSize: 12 },
1839
+ children: [copy.proReviewsFlash, mode === "review-pipeline" ? `(${copy.proReviewsLocked})` : ""]
1840
+ })]
1841
+ })] }))
1842
+ ] });
1843
+ })(),
1844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1845
+ style: S.group,
1846
+ children: copy.groupDispatch
1847
+ }),
1848
+ block(copy.cardDispatch, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1849
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1850
+ style: S.field,
1851
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1852
+ style: S.fieldLabel,
1853
+ children: copy.tier
1854
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1855
+ value: config.default_tier,
1856
+ onChange: (v) => field("default_tier", v),
1857
+ options: [{ value: "flash" }, { value: "pro" }]
1858
+ })]
1859
+ }),
1860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1861
+ style: S.field,
1862
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1863
+ style: S.fieldLabel,
1864
+ children: copy.effort
1865
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1866
+ value: config.default_effort,
1867
+ onChange: (v) => field("default_effort", v),
1868
+ options: [
1869
+ { value: "off" },
1870
+ { value: "high" },
1871
+ { value: "max" }
1872
+ ]
1873
+ })]
1874
+ }),
1875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1876
+ style: S.field,
1877
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1878
+ style: S.fieldLabel,
1879
+ children: copy.tierPolicy
1880
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1881
+ value: config.tier_policy,
1882
+ onChange: (v) => field("tier_policy", v),
1883
+ options: [
1884
+ "auto",
1885
+ "flash-only",
1886
+ "pro-only"
1887
+ ].map((p) => ({
1888
+ value: p,
1889
+ label: copy.tierPolicyDesc[p]
1890
+ }))
1891
+ })]
1892
+ }),
1893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1894
+ style: {
1895
+ ...S.field,
1896
+ flexDirection: "row",
1897
+ alignItems: "center",
1898
+ gap: 6,
1899
+ paddingBottom: 5
1900
+ },
1901
+ title: copy.escalateHint,
1902
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1903
+ type: "checkbox",
1904
+ checked: !!config.escalate_on_failure,
1905
+ onChange: (e) => field("escalate_on_failure", e.target.checked)
1906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1907
+ style: { fontSize: 12 },
1908
+ children: copy.escalate
1909
+ })]
1910
+ }),
1911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1912
+ style: S.field,
1913
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1914
+ style: S.fieldLabel,
1915
+ children: copy.presetFlash
1916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1917
+ value: config.preset_flash ?? "default",
1918
+ onChange: (v) => field("preset_flash", v),
1919
+ options: presetOptions
1920
+ })]
1921
+ }),
1922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1923
+ style: S.field,
1924
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1925
+ style: S.fieldLabel,
1926
+ children: copy.presetPro
1927
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1928
+ value: config.preset_pro ?? "default",
1929
+ onChange: (v) => field("preset_pro", v),
1930
+ options: presetOptions
1931
+ })]
1932
+ })
1933
+ ] })),
1934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1935
+ style: S.group,
1936
+ children: copy.groupRuntime
1937
+ }),
1938
+ block(copy.cardRuntime, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1939
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1940
+ style: S.field,
1941
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1942
+ style: S.fieldLabel,
1943
+ children: copy.mode
1944
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
1945
+ value: config.mode,
1946
+ onChange: (v) => field("mode", v),
1947
+ options: [
1948
+ "auto",
1949
+ "hub",
1950
+ "standalone"
1951
+ ].map((m) => ({
1952
+ value: m,
1953
+ label: copy.modeDesc[m]
1954
+ }))
1955
+ })]
1956
+ }),
1957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1958
+ style: S.field,
1959
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1960
+ style: S.fieldLabel,
1961
+ children: copy.timeout
1962
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1963
+ style: {
1964
+ ...S.input,
1965
+ width: "100%",
1966
+ boxSizing: "border-box"
1967
+ },
1968
+ type: "number",
1969
+ min: 60,
1970
+ max: 7200,
1971
+ value: config.default_timeout_seconds,
1972
+ onChange: (e) => fieldLocal("default_timeout_seconds", Number(e.target.value)),
1973
+ onBlur: () => applyPatch({ default_timeout_seconds: config.default_timeout_seconds })
1974
+ })]
1975
+ }),
1976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1977
+ style: S.field,
1978
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1979
+ style: S.fieldLabel,
1980
+ children: copy.hubUrl
1981
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1982
+ style: {
1983
+ ...S.input,
1984
+ width: "100%",
1985
+ boxSizing: "border-box"
1986
+ },
1987
+ value: config.hub_url,
1988
+ onChange: (e) => fieldLocal("hub_url", e.target.value),
1989
+ onBlur: () => applyPatch({ hub_url: config.hub_url }),
1990
+ onKeyDown: (e) => {
1991
+ if (e.key === "Enter") e.currentTarget.blur();
1992
+ }
1993
+ })]
1994
+ })
1995
+ ] })),
1996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1997
+ style: S.group,
1998
+ children: copy.groupMultimodal
1999
+ }),
2000
+ block(copy.cardMM, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2002
+ style: {
2003
+ ...S.field,
2004
+ flexDirection: "row",
2005
+ alignItems: "center",
2006
+ gap: 6,
2007
+ gridColumn: "1 / -1"
2008
+ },
2009
+ title: copy.capRestartHint,
2010
+ children: [
2011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2012
+ type: "checkbox",
2013
+ checked: !!config.vision_enabled,
2014
+ onChange: (e) => field("vision_enabled", e.target.checked)
2015
+ }),
2016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2017
+ style: { fontSize: 12.5 },
2018
+ children: copy.enableCrewVision
2019
+ }),
2020
+ config.vision_enabled === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2021
+ style: {
2022
+ fontSize: 11,
2023
+ opacity: .55
2024
+ },
2025
+ children: copy.capDisabledNote
2026
+ })
2027
+ ]
2028
+ }),
2029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2030
+ style: S.field,
2031
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2032
+ style: S.fieldLabel,
2033
+ children: copy.visionProvider
2034
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2035
+ value: config.vision_provider,
2036
+ onChange: (p) => {
2037
+ const custom = customProviders.find((c) => c.id === p);
2038
+ const m = custom ? custom.models?.[0] ?? "default" : (VISION_MODELS[p] ?? ["default"])[0];
2039
+ setModelAdd(null);
2040
+ setConfig((c) => ({
2041
+ ...c,
2042
+ vision_provider: p,
2043
+ vision_model: m
2044
+ }));
2045
+ applyPatch({
2046
+ vision_provider: p,
2047
+ vision_model: m
2048
+ });
2049
+ if ((p === "grok" || p === "agy") && !dynModels[p]) get(`/vision-models?provider=${p}`).then((r) => {
2050
+ if (r.ok) setDynModels((d) => ({
2051
+ ...d,
2052
+ [p]: r.models
2053
+ }));
2054
+ }).catch(() => {});
2055
+ },
2056
+ options: visionProviderOptions
2057
+ })]
2058
+ }),
2059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2060
+ style: S.field,
2061
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2062
+ style: S.fieldLabel,
2063
+ children: copy.visionModel
2064
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2065
+ style: {
2066
+ display: "flex",
2067
+ gap: 6,
2068
+ alignItems: "center"
2069
+ },
2070
+ children: [
2071
+ modelAdd === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2072
+ value: config.vision_model,
2073
+ onChange: (v) => field("vision_model", v),
2074
+ options: visionModelOptions
2075
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2076
+ autoFocus: true,
2077
+ style: {
2078
+ ...S.input,
2079
+ width: "100%",
2080
+ boxSizing: "border-box",
2081
+ ...S.mono
2082
+ },
2083
+ value: modelAdd,
2084
+ placeholder: copy.modelPlaceholder,
2085
+ onChange: (e) => setModelAdd(e.target.value),
2086
+ onBlur: commitModelAdd,
2087
+ onKeyDown: (e) => {
2088
+ if (e.key === "Enter") e.currentTarget.blur();
2089
+ if (e.key === "Escape") setModelAdd(null);
2090
+ }
2091
+ }),
2092
+ modelAdd === null && (config.vision_provider === "grok" || config.vision_provider === "agy") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2093
+ type: "button",
2094
+ title: copy.refreshModels,
2095
+ disabled: busy,
2096
+ style: {
2097
+ ...S.btn,
2098
+ padding: "3px 8px",
2099
+ flexShrink: 0
2100
+ },
2101
+ onClick: () => {
2102
+ const p = config.vision_provider;
2103
+ setBusy(true);
2104
+ get(`/vision-models?provider=${p}&refresh=1`).then((r) => {
2105
+ if (r.ok) setDynModels((d) => ({
2106
+ ...d,
2107
+ [p]: r.models
2108
+ }));
2109
+ }).catch(() => {}).finally(() => setBusy(false));
2110
+ },
2111
+ children: "↻"
2112
+ }),
2113
+ modelAdd === null && config.vision_provider !== "off" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2114
+ type: "button",
2115
+ title: copy.addModelTip,
2116
+ style: {
2117
+ ...S.btn,
2118
+ padding: "3px 8px",
2119
+ flexShrink: 0
2120
+ },
2121
+ onClick: () => setModelAdd(""),
2122
+ children: "+"
2123
+ }),
2124
+ modelAdd === null && (extraModels[config.vision_provider] ?? []).includes(config.vision_model) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2125
+ type: "button",
2126
+ title: copy.removeModelTip,
2127
+ style: {
2128
+ ...S.btn,
2129
+ padding: "3px 8px",
2130
+ flexShrink: 0
2131
+ },
2132
+ onClick: removeCurrentModel,
2133
+ children: "−"
2134
+ })
2135
+ ]
2136
+ })]
2137
+ }),
2138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2139
+ style: {
2140
+ ...S.field,
2141
+ flexDirection: "row",
2142
+ alignItems: "center",
2143
+ gap: 6,
2144
+ gridColumn: "1 / -1"
2145
+ },
2146
+ title: copy.capRestartHint,
2147
+ children: [
2148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2149
+ type: "checkbox",
2150
+ checked: !!config.imagegen_enabled,
2151
+ onChange: (e) => field("imagegen_enabled", e.target.checked)
2152
+ }),
2153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2154
+ style: { fontSize: 12.5 },
2155
+ children: copy.enableImagegen
2156
+ }),
2157
+ config.imagegen_enabled === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2158
+ style: {
2159
+ fontSize: 11,
2160
+ opacity: .55
2161
+ },
2162
+ children: copy.capDisabledNote
2163
+ })
2164
+ ]
2165
+ }),
2166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2167
+ style: S.field,
2168
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2169
+ style: S.fieldLabel,
2170
+ children: copy.imagegenProvider
2171
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2172
+ value: config.imagegen_provider,
2173
+ onChange: (v) => field("imagegen_provider", v),
2174
+ options: imagegenProviderOptions
2175
+ })]
2176
+ })
2177
+ ] })),
2178
+ block(copy.cardCustomProv, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2179
+ customProviders.length === 0 && provForm === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2180
+ style: {
2181
+ fontSize: 12,
2182
+ opacity: .5
2183
+ },
2184
+ children: copy.noCustomProviders
2185
+ }),
2186
+ customProviders.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2187
+ style: {
2188
+ display: "flex",
2189
+ alignItems: "center",
2190
+ gap: 8,
2191
+ fontSize: 12.5
2192
+ },
2193
+ children: [
2194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2195
+ style: { fontWeight: 500 },
2196
+ children: p.name || p.id
2197
+ }),
2198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2199
+ style: {
2200
+ ...S.chip(false),
2201
+ opacity: .7
2202
+ },
2203
+ children: provType(p) === "api" ? "API" : "CLI"
2204
+ }),
2205
+ canSee(p) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2206
+ style: S.chip(true),
2207
+ children: copy.capVision
2208
+ }),
2209
+ canDraw(p) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2210
+ style: S.chip(true),
2211
+ children: copy.capImagegen
2212
+ }),
2213
+ provType(p) === "api" && !!p.api_key && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2214
+ style: {
2215
+ fontSize: 11.5,
2216
+ opacity: .5
2217
+ },
2218
+ children: ["key ", copy.keySet]
2219
+ }),
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
2221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2222
+ style: {
2223
+ ...S.btn,
2224
+ padding: "2px 8px"
2225
+ },
2226
+ title: copy.testTip,
2227
+ disabled: !!testResult?.busy,
2228
+ onClick: () => runProviderTest(`row:${p.id}`, p),
2229
+ children: testResult?.key === `row:${p.id}` && testResult.busy ? copy.testing : copy.testProvider
2230
+ }),
2231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2232
+ style: {
2233
+ ...S.btn,
2234
+ padding: "2px 8px"
2235
+ },
2236
+ onClick: () => setProvForm({
2237
+ originalId: p.id,
2238
+ name: p.name ?? p.id,
2239
+ type: provType(p),
2240
+ models: (p.models ?? []).join(", "),
2241
+ base_url: p.base_url ?? "",
2242
+ api_key: p.api_key ?? "",
2243
+ imagegen_model: p.imagegen_model ?? "",
2244
+ vision_command: p.vision_command ?? "",
2245
+ imagegen_command: p.imagegen_command ?? ""
2246
+ }),
2247
+ children: copy.edit
2248
+ }),
2249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2250
+ style: {
2251
+ ...S.btn,
2252
+ padding: "2px 8px",
2253
+ opacity: .75
2254
+ },
2255
+ onClick: () => {
2256
+ if (confirm(copy.confirmDeleteProvider(p.name || p.id))) deleteProvider(p.id);
2257
+ },
2258
+ children: copy.del
2259
+ })
2260
+ ]
2261
+ }, p.id)),
2262
+ customProviders.some((p) => testResult?.key === `row:${p.id}`) && testReport(testResult.key),
2263
+ provForm === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2264
+ style: S.btn,
2265
+ onClick: () => setProvForm({
2266
+ name: "",
2267
+ type: "api",
2268
+ models: "",
2269
+ base_url: "",
2270
+ api_key: "",
2271
+ imagegen_model: "",
2272
+ vision_command: "",
2273
+ imagegen_command: ""
2274
+ }),
2275
+ children: copy.addProvider
2276
+ }) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2277
+ style: {
2278
+ borderTop: "1px solid rgba(128,128,128,0.16)",
2279
+ paddingTop: 10,
2280
+ display: "flex",
2281
+ flexDirection: "column",
2282
+ gap: 10
2283
+ },
2284
+ children: [
2285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2286
+ style: S.blockGrid,
2287
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2288
+ style: S.field,
2289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2290
+ style: S.fieldLabel,
2291
+ children: copy.providerName
2292
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2293
+ autoFocus: true,
2294
+ style: {
2295
+ ...S.input,
2296
+ width: "100%",
2297
+ boxSizing: "border-box"
2298
+ },
2299
+ value: provForm.name,
2300
+ onChange: (e) => setProvForm({
2301
+ ...provForm,
2302
+ name: e.target.value
2303
+ })
2304
+ })]
2305
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2306
+ style: S.field,
2307
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2308
+ style: S.fieldLabel,
2309
+ children: copy.providerType
2310
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2311
+ value: provForm.type,
2312
+ onChange: (v) => setProvForm({
2313
+ ...provForm,
2314
+ type: v
2315
+ }),
2316
+ options: [{
2317
+ value: "api",
2318
+ label: copy.typeApi
2319
+ }, {
2320
+ value: "cli",
2321
+ label: copy.typeCli
2322
+ }]
2323
+ })]
2324
+ })]
2325
+ }),
2326
+ provForm.type === "api" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2327
+ style: S.blockGrid,
2328
+ children: [
2329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2330
+ style: S.field,
2331
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2332
+ style: S.fieldLabel,
2333
+ children: copy.baseUrl
2334
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2335
+ style: {
2336
+ ...S.input,
2337
+ width: "100%",
2338
+ boxSizing: "border-box",
2339
+ ...S.mono
2340
+ },
2341
+ value: provForm.base_url,
2342
+ placeholder: "https://api.example.com/v1",
2343
+ onChange: (e) => setProvForm({
2344
+ ...provForm,
2345
+ base_url: e.target.value
2346
+ })
2347
+ })]
2348
+ }),
2349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2350
+ style: S.field,
2351
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2352
+ style: S.fieldLabel,
2353
+ children: copy.apiKey
2354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2355
+ type: "password",
2356
+ autoComplete: "off",
2357
+ style: {
2358
+ ...S.input,
2359
+ width: "100%",
2360
+ boxSizing: "border-box",
2361
+ ...S.mono
2362
+ },
2363
+ value: provForm.api_key,
2364
+ placeholder: copy.keyPlaceholder,
2365
+ onChange: (e) => setProvForm({
2366
+ ...provForm,
2367
+ api_key: e.target.value
2368
+ })
2369
+ })]
2370
+ }),
2371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2372
+ style: S.field,
2373
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2374
+ style: S.fieldLabel,
2375
+ children: copy.modelsField
2376
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2377
+ style: {
2378
+ ...S.input,
2379
+ width: "100%",
2380
+ boxSizing: "border-box",
2381
+ ...S.mono
2382
+ },
2383
+ value: provForm.models,
2384
+ placeholder: "gpt-4o-mini, qwen-vl-max",
2385
+ onChange: (e) => setProvForm({
2386
+ ...provForm,
2387
+ models: e.target.value
2388
+ })
2389
+ })]
2390
+ }),
2391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2392
+ style: S.field,
2393
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2394
+ style: S.fieldLabel,
2395
+ children: copy.imagegenModel
2396
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2397
+ style: {
2398
+ ...S.input,
2399
+ width: "100%",
2400
+ boxSizing: "border-box",
2401
+ ...S.mono
2402
+ },
2403
+ value: provForm.imagegen_model,
2404
+ placeholder: "dall-e-3",
2405
+ onChange: (e) => setProvForm({
2406
+ ...provForm,
2407
+ imagegen_model: e.target.value
2408
+ })
2409
+ })]
2410
+ })
2411
+ ]
2412
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2413
+ style: {
2414
+ fontSize: 11.5,
2415
+ opacity: .55
2416
+ },
2417
+ children: copy.apiFormHint
2418
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2419
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2420
+ style: S.field,
2421
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2422
+ style: S.fieldLabel,
2423
+ children: copy.visionCmd
2424
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2425
+ style: {
2426
+ ...S.input,
2427
+ width: "100%",
2428
+ boxSizing: "border-box",
2429
+ ...S.mono
2430
+ },
2431
+ value: provForm.vision_command,
2432
+ placeholder: "mycli see {image} --ask {question} --model {model}",
2433
+ onChange: (e) => setProvForm({
2434
+ ...provForm,
2435
+ vision_command: e.target.value
2436
+ })
2437
+ })]
2438
+ }),
2439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2440
+ style: S.field,
2441
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2442
+ style: S.fieldLabel,
2443
+ children: copy.imagegenCmd
2444
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2445
+ style: {
2446
+ ...S.input,
2447
+ width: "100%",
2448
+ boxSizing: "border-box",
2449
+ ...S.mono
2450
+ },
2451
+ value: provForm.imagegen_command,
2452
+ placeholder: "mycli gen {prompt} -o {output}",
2453
+ onChange: (e) => setProvForm({
2454
+ ...provForm,
2455
+ imagegen_command: e.target.value
2456
+ })
2457
+ })]
2458
+ }),
2459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2460
+ style: S.field,
2461
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2462
+ style: S.fieldLabel,
2463
+ children: copy.modelsField
2464
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2465
+ style: {
2466
+ ...S.input,
2467
+ width: "100%",
2468
+ boxSizing: "border-box",
2469
+ ...S.mono
2470
+ },
2471
+ value: provForm.models,
2472
+ placeholder: "model-a, model-b",
2473
+ onChange: (e) => setProvForm({
2474
+ ...provForm,
2475
+ models: e.target.value
2476
+ })
2477
+ })]
2478
+ }),
2479
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2480
+ style: {
2481
+ fontSize: 11.5,
2482
+ opacity: .55
2483
+ },
2484
+ children: copy.cliFormHint
2485
+ })
2486
+ ] }),
2487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2488
+ style: {
2489
+ display: "flex",
2490
+ gap: 8,
2491
+ alignItems: "center"
2492
+ },
2493
+ children: [
2494
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2495
+ style: S.btn,
2496
+ onClick: saveProviderForm,
2497
+ children: copy.saveProvider
2498
+ }),
2499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2500
+ style: S.btn,
2501
+ title: copy.testTip,
2502
+ disabled: !!testResult?.busy,
2503
+ onClick: () => runProviderTest("form", {
2504
+ id: provForm.originalId ?? "draft",
2505
+ name: provForm.name || "draft",
2506
+ type: provForm.type,
2507
+ models: provForm.models.split(/[,,\n]+/).map((x) => x.trim()).filter(Boolean),
2508
+ base_url: provForm.base_url.trim(),
2509
+ api_key: provForm.api_key,
2510
+ imagegen_model: provForm.imagegen_model.trim(),
2511
+ vision_command: provForm.vision_command.trim(),
2512
+ imagegen_command: provForm.imagegen_command.trim()
2513
+ }),
2514
+ children: testResult?.key === "form" && testResult.busy ? copy.testing : copy.testProvider
2515
+ }),
2516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2517
+ style: {
2518
+ ...S.btn,
2519
+ opacity: .75
2520
+ },
2521
+ onClick: () => setProvForm(null),
2522
+ children: copy.cancel
2523
+ })
2524
+ ]
2525
+ }),
2526
+ testReport("form")
2527
+ ]
2528
+ })
2529
+ ] }), false)
2530
+ ] }),
2531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2532
+ style: {
2533
+ fontSize: 11.5,
2534
+ opacity: .55,
2535
+ marginTop: 2
2536
+ },
2537
+ children: copy.globalHint
2538
+ }),
2539
+ notice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2540
+ style: {
2541
+ fontSize: 12,
2542
+ opacity: .8,
2543
+ whiteSpace: "pre-wrap"
2544
+ },
2545
+ children: notice
2546
+ }),
2547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2548
+ style: S.section,
2549
+ children: copy.jobs
2550
+ }),
2551
+ jobs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2552
+ style: { opacity: .55 },
2553
+ children: copy.empty
2554
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2555
+ style: { overflowX: "auto" },
2556
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
2557
+ style: {
2558
+ borderCollapse: "collapse",
2559
+ width: "100%",
2560
+ tableLayout: "fixed",
2561
+ minWidth: 760
2562
+ },
2563
+ children: [
2564
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("colgroup", { children: [
2565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 64 } }),
2566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 58 } }),
2567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 72 } }),
2568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 40 } }),
2569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 100 } }),
2570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 84 } }),
2571
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: { width: 340 } })
2572
+ ] }),
2573
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: [
2574
+ copy.col.id,
2575
+ copy.col.source,
2576
+ copy.col.tier,
2577
+ copy.col.status,
2578
+ copy.col.progress,
2579
+ copy.col.tokens,
2580
+ copy.col.task
2581
+ ].map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
2582
+ style: {
2583
+ ...S.tight,
2584
+ fontSize: 11,
2585
+ opacity: .55,
2586
+ fontWeight: 500
2587
+ },
2588
+ children: h
2589
+ }, h)) }) }),
2590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: jobs.map((job) => {
2591
+ const color = job.status === "running" ? "#4a9eff" : job.status === "done" ? "#3fb950" : "#f85149";
2592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
2593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2594
+ style: {
2595
+ ...S.tight,
2596
+ ...S.mono
2597
+ },
2598
+ title: job.id,
2599
+ children: String(job.id).replace(/-[a-z0-9]+$/, "")
2600
+ }),
2601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2602
+ style: S.tight,
2603
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2604
+ style: S.chip(job.source === "claude-code" || job.source === "codex"),
2605
+ children: sourceLabel(job.source)
2606
+ })
2607
+ }),
2608
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
2609
+ style: S.tight,
2610
+ children: [
2611
+ job.tier,
2612
+ "/",
2613
+ job.effort
2614
+ ]
2615
+ }),
2616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2617
+ style: {
2618
+ ...S.tight,
2619
+ textAlign: "center"
2620
+ },
2621
+ title: `${job.status}${job.currentTool ? ` · ${job.currentTool}` : ""}`,
2622
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2623
+ style: {
2624
+ color,
2625
+ cursor: "default"
2626
+ },
2627
+ children: "●"
2628
+ })
2629
+ }),
2630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
2631
+ style: {
2632
+ ...S.tight,
2633
+ ...S.mono
2634
+ },
2635
+ title: copy.progressTip(elapsed(job.startedAt, job.endedAt), job.turn, job.step, job.toolCalls ?? "-"),
2636
+ children: [
2637
+ elapsed(job.startedAt, job.endedAt),
2638
+ " ",
2639
+ job.turn,
2640
+ ".",
2641
+ job.step,
2642
+ " ",
2643
+ job.toolCalls ?? "-",
2644
+ "t"
2645
+ ]
2646
+ }),
2647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2648
+ style: {
2649
+ ...S.tight,
2650
+ ...S.mono
2651
+ },
2652
+ children: job.tokens ? `${ktok(job.tokens.input)}/${ktok(job.tokens.output)}` : "-"
2653
+ }),
2654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2655
+ style: {
2656
+ ...S.cell,
2657
+ position: "relative"
2658
+ },
2659
+ onMouseEnter: (e) => {
2660
+ const rect = e.currentTarget.getBoundingClientRect();
2661
+ setHoverTask({
2662
+ id: job.id,
2663
+ text: job.task,
2664
+ right: Math.max(8, window.innerWidth - rect.right),
2665
+ top: rect.bottom + 4,
2666
+ bottom: window.innerHeight - rect.top + 4,
2667
+ flip: rect.top < 240
2668
+ });
2669
+ },
2670
+ onMouseLeave: () => setHoverTask(null),
2671
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2672
+ style: {
2673
+ whiteSpace: "nowrap",
2674
+ overflow: "hidden",
2675
+ textOverflow: "ellipsis",
2676
+ lineHeight: 1.45,
2677
+ fontSize: 12.5
2678
+ },
2679
+ children: job.task
2680
+ })
2681
+ })
2682
+ ] }, job.id);
2683
+ }) })
2684
+ ]
2685
+ })
2686
+ }),
2687
+ hoverTask && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2688
+ style: {
2689
+ position: "fixed",
2690
+ right: hoverTask.right,
2691
+ zIndex: 9999,
2692
+ ...hoverTask.flip ? { top: hoverTask.top } : { bottom: hoverTask.bottom },
2693
+ minWidth: 260,
2694
+ maxWidth: 420,
2695
+ maxHeight: 180,
2696
+ overflowY: "auto",
2697
+ padding: "8px 12px",
2698
+ borderRadius: 8,
2699
+ border: "1px solid rgba(128,128,128,0.35)",
2700
+ background: "Canvas",
2701
+ color: "CanvasText",
2702
+ boxShadow: "0 8px 28px rgba(0,0,0,0.22)",
2703
+ whiteSpace: "pre-wrap",
2704
+ fontSize: 12,
2705
+ lineHeight: 1.5
2706
+ },
2707
+ children: hoverTask.text
2708
+ }), document.body)
2709
+ ]
2710
+ });
2711
+ }
2712
+ function apply(ctx) {
2713
+ let runningCount = 0;
2714
+ ctx.slots.inject("settings.section", () => {
2715
+ let dispose = register();
2716
+ function register() {
2717
+ return ctx.slots.register({
2718
+ name: "settings.section",
2719
+ id: "dsh-crew",
2720
+ order: 65,
2721
+ label: () => {
2722
+ const base = COPY[ctx.locale.getLocale().active === "zh" ? "zh" : "en"].label;
2723
+ if (runningCount <= 0) return base;
2724
+ return `${base} ${[
2725
+ "❶",
2726
+ "❷",
2727
+ "❸",
2728
+ "❹",
2729
+ "❺",
2730
+ "❻",
2731
+ "❼",
2732
+ "❽",
2733
+ "❾",
2734
+ "❿"
2735
+ ][runningCount - 1] ?? `(${runningCount})`}`;
2736
+ }
2737
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkersPanel, { ctx }));
2738
+ }
2739
+ const poll = async () => {
2740
+ if (document.visibilityState !== "visible") return;
2741
+ try {
2742
+ const r = await (await fetch(`${API}/jobs`, { cache: "no-store" })).json();
2743
+ const n = r.ok ? (r.jobs ?? []).filter((j) => j.status === "running").length : 0;
2744
+ if (n !== runningCount) {
2745
+ runningCount = n;
2746
+ dispose();
2747
+ dispose = register();
2748
+ }
2749
+ } catch {}
2750
+ };
2751
+ poll();
2752
+ const timer = setInterval(() => {
2753
+ poll();
2754
+ }, 1e4);
2755
+ return () => {
2756
+ clearInterval(timer);
2757
+ dispose();
2758
+ };
2759
+ });
2760
+ }
2761
+ //#endregion
2762
+ exports.apply = apply;
2763
+ exports.inject = inject;
2764
+
2765
+ return module.exports; } });