@ran-sh/dsh-crew 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/docs/ui-surfaces.md +47 -0
- package/lib/client.js +978 -498
- package/official-web-bridge/lib/client.js +978 -498
- package/package.json +5 -4
- package/src/client/collapsible-sections.mjs +4 -3
- package/src/client/entry.tsx +9 -236
- package/src/client/host-readiness.mjs +45 -0
- package/src/client/index.tsx +368 -123
- package/src/client/surface-detection.mjs +30 -0
- package/src/client/task-telemetry.mjs +72 -0
- package/src/install/install-legacy.mjs +113 -14
- package/src/official-web-bridge.mjs +6 -1
- package/src/runtime-identity.mjs +6 -4
package/src/client/index.tsx
CHANGED
|
@@ -4,31 +4,60 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
6
6
|
import { createPortal } from 'react-dom';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
8
|
openSections,
|
|
9
9
|
readSectionState,
|
|
10
10
|
setEverySection,
|
|
11
11
|
writeSectionState,
|
|
12
|
-
} from './collapsible-sections.mjs';
|
|
12
|
+
} from './collapsible-sections.mjs';
|
|
13
|
+
import { ActivationSummary } from './activation-summary';
|
|
14
|
+
import { projectHostReadiness, READINESS_STATES } from './host-readiness.mjs';
|
|
15
|
+
import { CREW_UI_SURFACES, classifyCrewSurface, surfaceResponsibilities } from './surface-detection.mjs';
|
|
16
|
+
import { aggregateModelInvocations } from './task-telemetry.mjs';
|
|
13
17
|
|
|
14
18
|
export const inject = ['slots', 'locale'];
|
|
15
19
|
|
|
16
|
-
const API = '/_dsh/dsh-crew';
|
|
20
|
+
const API = '/_dsh/dsh-crew';
|
|
21
|
+
const CREW_HARNESS_URL = 'http://127.0.0.1:3210/';
|
|
22
|
+
const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3080/';
|
|
23
|
+
|
|
24
|
+
type AdaptiveConfig = { enabled: boolean; window_size: number; min_samples: number };
|
|
25
|
+
const DEFAULT_ADAPTIVE: AdaptiveConfig = { enabled: false, window_size: 8, min_samples: 2 };
|
|
26
|
+
|
|
27
|
+
function clampInt(value: unknown, fallback: number, min: number, max: number) {
|
|
28
|
+
const parsed = Number(value);
|
|
29
|
+
if (!Number.isInteger(parsed)) return fallback;
|
|
30
|
+
return Math.max(min, Math.min(max, parsed));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeAdaptive(value: any): AdaptiveConfig {
|
|
34
|
+
const windowSize = clampInt(value?.window_size, DEFAULT_ADAPTIVE.window_size, 1, 32);
|
|
35
|
+
return {
|
|
36
|
+
enabled: value?.enabled === true,
|
|
37
|
+
window_size: windowSize,
|
|
38
|
+
min_samples: clampInt(value?.min_samples, DEFAULT_ADAPTIVE.min_samples, 1, windowSize),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
17
41
|
|
|
18
42
|
const COPY = {
|
|
19
43
|
zh: {
|
|
20
44
|
label: 'DSH Crew',
|
|
21
45
|
title: 'DSH Crew',
|
|
22
|
-
intro: '
|
|
46
|
+
intro: '日常 Crew 控制台:在一个入口里配置编排、角色、模型路由与宿主集成,并跟踪真实 Worker / Reviewer 任务。',
|
|
23
47
|
integrations: '集成',
|
|
24
|
-
installed: '已安装', notInstalled: '未安装', hud: 'HUD 段',
|
|
48
|
+
installed: '已安装', notInstalled: '未安装', ready: '可调用', notReady: '未就绪', hud: 'HUD 段',
|
|
25
49
|
install: '安装', update: '更新', restore: '还原',
|
|
26
50
|
confirmRestore: (name: string) => `确定从 ${name} 移除 dsh-crew 集成?(settings 会先备份)`,
|
|
27
51
|
globalConfig: '全局配置',
|
|
28
52
|
expandAll: '全部展开', collapseAll: '全部折叠',
|
|
29
53
|
modelCount: (count: number) => `${count} 个模型`, providerCount: (count: number) => `${count} 个 Provider`,
|
|
30
54
|
jobCount: (count: number) => `${count} 个任务`, runningCount: (count: number) => `${count} 个运行中`,
|
|
31
|
-
sectionNames: { integrations: '
|
|
55
|
+
sectionNames: { integrations: 'Codex / Claude 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', providers: '自定义 Provider', jobs: '任务状态' },
|
|
56
|
+
openHarness: '打开 3210 Crew Harness →',
|
|
57
|
+
harnessHint: '底层 Provider、Harness Models 与运行时配置',
|
|
58
|
+
hostReadiness: '宿主集成就绪度', hostReadinessHint: '只使用结构化安装与运行时证据;缺少证据不会显示 READY。',
|
|
59
|
+
readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', crew_harness: 'Crew Harness', official_bridge: 'Official bridge' },
|
|
60
|
+
readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN' },
|
|
32
61
|
globalHint: '修改即时保存到 ~/.config/dsh-crew/config.json;CC / Codex 的新会话自动读取为默认值(会话内可用 /dsh-crew:config 临时覆盖)。',
|
|
33
62
|
orchestration: 'Agent 编排',
|
|
34
63
|
enableSubagents: '启用子 Agent',
|
|
@@ -112,8 +141,12 @@ const COPY = {
|
|
|
112
141
|
cardMM: { t: '视觉与生图', d: '给纯文本的 DSH 模型借来眼睛和画笔:describe_image、会话贴图转写与 generate_image 都用这里的设置。' },
|
|
113
142
|
cardCustomProv: { t: '自定义 Provider', d: '接入自己的 API 或本地命令;保存后会出现在上面的视觉 / 生图 provider 选择里。' },
|
|
114
143
|
modeDesc: { auto: 'auto · 优先 hub', hub: 'hub · 必须 hub', standalone: 'standalone · 独立进程' },
|
|
115
|
-
save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有
|
|
116
|
-
|
|
144
|
+
save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
|
|
145
|
+
adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
|
|
146
|
+
adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
|
|
147
|
+
modelActivity: '模型调用概览', modelActivityHint: '聚合本 Hub 内存中最近 500 个任务的真实调用证据,最多显示 50 个模型;不保存提示词、结果或凭据。', noModelActivity: '还没有真实模型调用。',
|
|
148
|
+
calls: '调用次数', invocationSource: '任务来源', routingSource: '路由来源', lastCalled: '最近调用', role: '角色', model: '模型', never: '—',
|
|
149
|
+
col: { id: '任务', role: '角色', source: '来源', model: '模型', tier: '档位', status: '状态', progress: '进度', tokens: 'tokens ⇅', task: '内容' },
|
|
117
150
|
working: '处理中…',
|
|
118
151
|
tips: {
|
|
119
152
|
claude: {
|
|
@@ -131,16 +164,21 @@ const COPY = {
|
|
|
131
164
|
en: {
|
|
132
165
|
label: 'DSH Crew',
|
|
133
166
|
title: 'DSH Crew',
|
|
134
|
-
intro: '
|
|
167
|
+
intro: 'The daily Crew console: configure orchestration, roles, model routing, and host integrations in one place while tracking real Worker / Reviewer jobs.',
|
|
135
168
|
integrations: 'Integrations',
|
|
136
|
-
installed: 'installed', notInstalled: 'not installed', hud: 'HUD segment',
|
|
169
|
+
installed: 'installed', notInstalled: 'not installed', ready: 'ready', notReady: 'not ready', hud: 'HUD segment',
|
|
137
170
|
install: 'Install', update: 'Update', restore: 'Restore',
|
|
138
171
|
confirmRestore: (name: string) => `Remove the dsh-crew integration from ${name}? (settings are backed up first)`,
|
|
139
172
|
globalConfig: 'Global configuration',
|
|
140
173
|
expandAll: 'Expand all', collapseAll: 'Collapse all',
|
|
141
174
|
modelCount: (count: number) => `${count} models`, providerCount: (count: number) => `${count} providers`,
|
|
142
175
|
jobCount: (count: number) => `${count} jobs`, runningCount: (count: number) => `${count} running`,
|
|
143
|
-
sectionNames: { integrations: '
|
|
176
|
+
sectionNames: { integrations: 'Codex / Claude integration status', workflow: 'Crew workflow settings', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: 'Model priority & dispatch', adaptive: 'Adaptive routing', runtime: 'Runtime / activation boundaries', multimodal: 'Vision & image generation', providers: 'Custom providers', jobs: 'Task status' },
|
|
177
|
+
openHarness: 'Open 3210 Crew Harness →',
|
|
178
|
+
harnessHint: 'Low-level providers, Harness Models, and runtime configuration',
|
|
179
|
+
hostReadiness: 'Host integration readiness', hostReadinessHint: 'Uses structured installer and runtime evidence only; missing evidence is never READY.',
|
|
180
|
+
readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', crew_harness: 'Crew Harness', official_bridge: 'Official bridge' },
|
|
181
|
+
readinessStates: { READY: 'READY', DEGRADED: 'DEGRADED', UNAVAILABLE: 'UNAVAILABLE', UNKNOWN: 'UNKNOWN' },
|
|
144
182
|
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).',
|
|
145
183
|
orchestration: 'Agent orchestration',
|
|
146
184
|
enableSubagents: 'Enable Subagents',
|
|
@@ -224,8 +262,12 @@ const COPY = {
|
|
|
224
262
|
cardMM: { t: 'Vision & image generation', d: "Lends the harness's text-only models eyes and a brush: describe_image, pasted-image transcription and generate_image all use these settings." },
|
|
225
263
|
cardCustomProv: { t: 'Custom providers', d: 'Bring your own API or local command; saved providers appear in the vision / image-gen selects above.' },
|
|
226
264
|
modeDesc: { auto: 'auto · prefer hub', hub: 'hub · require hub', standalone: 'standalone' },
|
|
227
|
-
save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No
|
|
228
|
-
|
|
265
|
+
save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
|
|
266
|
+
adaptiveTitle: 'Adaptive model routing (experimental)', adaptiveHint: 'Off by default. Only system-derived candidates may be reordered; explicit model priorities always keep their order. Health uses only process-local success, failure, timeout, and coarse latency observations and resets on restart.',
|
|
267
|
+
adaptiveEnabled: 'Enable adaptive routing', adaptiveWindow: 'Health window', adaptiveSamples: 'Minimum samples', adaptiveBoundary: 'Effective for the next workflow',
|
|
268
|
+
modelActivity: 'Model invocation overview', modelActivityHint: 'Aggregates real invocation evidence from the latest 500 in-memory Hub jobs and shows at most 50 models; prompts, results, and credentials are never stored here.', noModelActivity: 'No real model invocations yet.',
|
|
269
|
+
calls: 'Calls', invocationSource: 'Task source', routingSource: 'Routing source', lastCalled: 'Last called', role: 'Role', model: 'Model', never: '—',
|
|
270
|
+
col: { id: 'job', role: 'role', source: 'source', model: 'model', tier: 'tier', status: 'status', progress: 'progress', tokens: 'tokens ⇅', task: 'task' },
|
|
229
271
|
working: 'Working…',
|
|
230
272
|
tips: {
|
|
231
273
|
claude: {
|
|
@@ -369,9 +411,63 @@ function elapsed(startedAt: string, endedAt?: string | null) {
|
|
|
369
411
|
const s = Math.max(0, Math.round(((endedAt ? +new Date(endedAt) : Date.now()) - +new Date(startedAt)) / 1000));
|
|
370
412
|
return s >= 60 ? `${Math.floor(s / 60)}m${s % 60}s` : `${s}s`;
|
|
371
413
|
}
|
|
372
|
-
function ktok(n: number) { return n >= 1000 ? `${Math.round(n / 100) / 10}k` : `${n}`; }
|
|
373
|
-
|
|
374
|
-
function
|
|
414
|
+
function ktok(n: number) { return n >= 1000 ? `${Math.round(n / 100) / 10}k` : `${n}`; }
|
|
415
|
+
|
|
416
|
+
function formatTimestamp(value: string | null, locale: string) {
|
|
417
|
+
if (!value || !Number.isFinite(Date.parse(value))) return '—';
|
|
418
|
+
return new Intl.DateTimeFormat(locale === 'zh' ? 'zh-CN' : 'en', {
|
|
419
|
+
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
420
|
+
}).format(new Date(value));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readinessChip(state: string) {
|
|
424
|
+
const color = state === READINESS_STATES.READY ? '#3fb950'
|
|
425
|
+
: state === READINESS_STATES.DEGRADED ? '#c98735'
|
|
426
|
+
: state === READINESS_STATES.UNAVAILABLE ? '#f85149' : 'inherit';
|
|
427
|
+
return {
|
|
428
|
+
fontSize: 10.5, fontWeight: 650, padding: '1px 7px', borderRadius: 99,
|
|
429
|
+
border: `1px solid ${state === READINESS_STATES.UNKNOWN ? 'rgba(128,128,128,0.35)' : color}`,
|
|
430
|
+
color, opacity: state === READINESS_STATES.UNKNOWN ? 0.58 : 1,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function MinimalCrewPanel({ locale, surface, runtime }: { locale: string; surface: string; runtime: any }) {
|
|
435
|
+
const zh = locale === 'zh';
|
|
436
|
+
const native = surface === CREW_UI_SURFACES.NATIVE;
|
|
437
|
+
const title = native ? (zh ? 'DSH Crew Runtime' : 'DSH Crew Runtime') : (zh ? 'DSH Crew Surface 未确认' : 'DSH Crew surface not verified');
|
|
438
|
+
const hint = native
|
|
439
|
+
? (zh
|
|
440
|
+
? '这里是隔离的 3210 Crew Harness。请使用 Harness 原生菜单管理 Provider、Harness Models、Agent 预设与底层配置;Crew 编排和宿主集成统一在 3080 管理。'
|
|
441
|
+
: 'This is the isolated 3210 Crew Harness. Use the native Harness menus for Providers, Harness Models, Agent presets, and low-level settings; manage Crew orchestration and host integrations on 3080.')
|
|
442
|
+
: (zh
|
|
443
|
+
? '无法用结构化后端证据确认当前界面,因此已保守隐藏 Crew 编排、任务与宿主集成功能。'
|
|
444
|
+
: 'Structured backend evidence could not identify this surface, so Crew orchestration, jobs, and host integrations are conservatively hidden.');
|
|
445
|
+
const runtimeReady = runtime?.ok === true && runtime?.service === 'dsh-crew-hub';
|
|
446
|
+
return (
|
|
447
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, fontSize: 13, lineHeight: 1.55 }}>
|
|
448
|
+
<div style={{ ...S.block, background: 'linear-gradient(135deg, rgba(74,158,255,0.08), rgba(128,128,128,0.02))' }}>
|
|
449
|
+
<div>
|
|
450
|
+
<div style={{ fontSize: 19, fontWeight: 680 }}>{title}</div>
|
|
451
|
+
<div style={{ fontSize: 12, opacity: 0.68, marginTop: 3 }}>{hint}</div>
|
|
452
|
+
</div>
|
|
453
|
+
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
|
|
454
|
+
<span style={readinessChip(runtimeReady ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
|
|
455
|
+
{runtimeReady ? `Runtime ${runtime.runtime_version ?? 'READY'}` : 'Runtime UNKNOWN'}
|
|
456
|
+
</span>
|
|
457
|
+
<span style={readinessChip(native ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN)}>
|
|
458
|
+
{native ? '3210 · isolated' : 'surface · unknown'}
|
|
459
|
+
</span>
|
|
460
|
+
</div>
|
|
461
|
+
<a href={CREW_CONTROL_PLANE_URL} target="_blank" rel="noopener noreferrer" style={{
|
|
462
|
+
...S.btn, alignSelf: 'flex-start', textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
|
|
463
|
+
borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.08)',
|
|
464
|
+
}}>{zh ? '打开 3080 DSH Crew 控制台 →' : 'Open the 3080 DSH Crew console →'}</a>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function WorkersPanel({ ctx }: { ctx: any }) {
|
|
375
471
|
const locale = useSyncExternalStore(
|
|
376
472
|
(notify: () => void) => ctx.on('locale/change', notify),
|
|
377
473
|
() => ctx.locale.getLocale().active,
|
|
@@ -398,8 +494,10 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
398
494
|
const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
|
|
399
495
|
const [modelPickerTier, setModelPickerTier] = useState<'flash' | 'pro' | null>(null);
|
|
400
496
|
const [modelQuery, setModelQuery] = useState('');
|
|
401
|
-
const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
|
|
402
|
-
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
|
|
497
|
+
const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
|
|
498
|
+
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => readSectionState(sectionStorage()));
|
|
499
|
+
const [surface, setSurface] = useState<string>('detecting');
|
|
500
|
+
const [runtimeInfo, setRuntimeInfo] = useState<any>(undefined);
|
|
403
501
|
|
|
404
502
|
const toggleSection = (sectionId: string) => {
|
|
405
503
|
setExpandedSections((current) => ({ ...current, [sectionId]: !current[sectionId] }));
|
|
@@ -445,10 +543,29 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
445
543
|
* (DSH's setting may be unset), and the hub localizes its own strings from it. */
|
|
446
544
|
const withLang = useCallback((path: string) => `${API}${path}${path.includes('?') ? '&' : '?'}lang=${locale === 'zh' ? 'zh' : 'en'}`, [locale]);
|
|
447
545
|
const get = useCallback(async (path: string) => readJson(await fetch(withLang(path), { cache: 'no-store' }), path), [readJson, withLang]);
|
|
448
|
-
const post = useCallback(async (path: string, body: any) => readJson(await fetch(withLang(path), {
|
|
546
|
+
const post = useCallback(async (path: string, body: any) => readJson(await fetch(withLang(path), {
|
|
449
547
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
450
548
|
body: JSON.stringify({ ...body, lang: locale === 'zh' ? 'zh' : 'en' }),
|
|
451
|
-
}), path), [readJson, withLang, locale]);
|
|
549
|
+
}), path), [readJson, withLang, locale]);
|
|
550
|
+
|
|
551
|
+
const detectSurface = useCallback(async () => {
|
|
552
|
+
const readOptional = async (path: string) => {
|
|
553
|
+
try {
|
|
554
|
+
const response = await fetch(`${API}${path}`, { cache: 'no-store' });
|
|
555
|
+
if (!response.ok) return null;
|
|
556
|
+
const body = await response.json();
|
|
557
|
+
return body && typeof body === 'object' ? body : null;
|
|
558
|
+
} catch { return null; }
|
|
559
|
+
};
|
|
560
|
+
const [bridgeStatus, runtime] = await Promise.all([
|
|
561
|
+
readOptional('/bridge-status'),
|
|
562
|
+
readOptional('/runtime'),
|
|
563
|
+
]);
|
|
564
|
+
setRuntimeInfo(runtime);
|
|
565
|
+
setSurface(classifyCrewSurface({ bridgeStatus, runtime }));
|
|
566
|
+
}, []);
|
|
567
|
+
|
|
568
|
+
useEffect(() => { void detectSurface(); }, [detectSurface]);
|
|
452
569
|
|
|
453
570
|
const refreshAll = useCallback(async () => {
|
|
454
571
|
try {
|
|
@@ -463,11 +580,12 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
463
580
|
} catch { /* instance restarting */ }
|
|
464
581
|
}, [get]);
|
|
465
582
|
|
|
466
|
-
useEffect(() => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
if (surface !== CREW_UI_SURFACES.OFFICIAL) return undefined;
|
|
585
|
+
void refreshAll();
|
|
586
|
+
const timer = setInterval(() => { void get('/jobs').then((j) => { if (j.ok) setJobs(j.jobs ?? []); }).catch(() => {}); }, 3000);
|
|
587
|
+
return () => clearInterval(timer);
|
|
588
|
+
}, [surface, refreshAll, get]);
|
|
471
589
|
|
|
472
590
|
const refreshHarnessModels = useCallback(async () => {
|
|
473
591
|
setModelCatalogBusy(true);
|
|
@@ -483,7 +601,9 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
483
601
|
}
|
|
484
602
|
}, [get, copy]);
|
|
485
603
|
|
|
486
|
-
useEffect(() => {
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
|
|
606
|
+
}, [surface, refreshHarnessModels]);
|
|
487
607
|
|
|
488
608
|
const act = useCallback(async (target: string, confirmName?: string) => {
|
|
489
609
|
if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
|
|
@@ -496,15 +616,27 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
496
616
|
} catch (e: any) { setNotice(String(e?.message ?? e)); } finally { setBusy(false); }
|
|
497
617
|
}, [post, get, copy]);
|
|
498
618
|
|
|
499
|
-
const applyPatch = useCallback((patch: Record<string, any>) => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
619
|
+
const applyPatch = useCallback(async (patch: Record<string, any>) => {
|
|
620
|
+
try {
|
|
621
|
+
const result = await post('/config', patch);
|
|
622
|
+
if (!result.ok) throw new Error(result.error ?? 'Configuration save failed');
|
|
623
|
+
setConfig(result.config);
|
|
624
|
+
setNotice(copy.saved);
|
|
625
|
+
setTimeout(() => setNotice(''), 1500);
|
|
626
|
+
return true;
|
|
627
|
+
} catch (error: any) {
|
|
628
|
+
setNotice(String(error?.message ?? error));
|
|
629
|
+
try {
|
|
630
|
+
const authoritative = await get('/config');
|
|
631
|
+
if (authoritative.ok) setConfig(authoritative.config);
|
|
632
|
+
} catch { /* keep the error visible while the Hub is unavailable */ }
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}, [post, get, copy]);
|
|
504
636
|
/** Selects & checkboxes: apply immediately. */
|
|
505
637
|
const field = (key: string, value: any) => {
|
|
506
638
|
setConfig((c: any) => ({ ...c, [key]: value }));
|
|
507
|
-
applyPatch({ [key]: value });
|
|
639
|
+
void applyPatch({ [key]: value });
|
|
508
640
|
};
|
|
509
641
|
/** Text/number inputs: track locally, apply on blur. */
|
|
510
642
|
const fieldLocal = (key: string, value: any) => setConfig((c: any) => ({ ...c, [key]: value }));
|
|
@@ -544,7 +676,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
544
676
|
const cur = extraModels[p] ?? [];
|
|
545
677
|
const nextExtra = { ...extraModels, [p]: cur.includes(m) ? cur : [...cur, m] };
|
|
546
678
|
setConfig((c: any) => ({ ...c, extra_models: nextExtra, vision_model: m }));
|
|
547
|
-
applyPatch({ extra_models: nextExtra, vision_model: m });
|
|
679
|
+
void applyPatch({ extra_models: nextExtra, vision_model: m });
|
|
548
680
|
};
|
|
549
681
|
const removeCurrentModel = () => {
|
|
550
682
|
if (!config) return;
|
|
@@ -553,7 +685,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
553
685
|
const nextExtra = { ...extraModels, [p]: (extraModels[p] ?? []).filter((x) => x !== m) };
|
|
554
686
|
const fallback = visionModelOptions.find((o) => o.value !== m)?.value ?? 'default';
|
|
555
687
|
setConfig((c: any) => ({ ...c, extra_models: nextExtra, vision_model: fallback }));
|
|
556
|
-
applyPatch({ extra_models: nextExtra, vision_model: fallback });
|
|
688
|
+
void applyPatch({ extra_models: nextExtra, vision_model: fallback });
|
|
557
689
|
};
|
|
558
690
|
const saveProviderForm = () => {
|
|
559
691
|
if (!provForm || !config) return;
|
|
@@ -621,7 +753,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
621
753
|
if (config.vision_provider === id) { patch.vision_provider = 'claude-code'; patch.vision_model = 'haiku'; }
|
|
622
754
|
if (config.imagegen_provider === id) patch.imagegen_provider = 'codex';
|
|
623
755
|
setConfig((c: any) => ({ ...c, ...patch }));
|
|
624
|
-
applyPatch(patch);
|
|
756
|
+
void applyPatch(patch);
|
|
625
757
|
};
|
|
626
758
|
|
|
627
759
|
/** Card: title + one-line description header, then its fields. */
|
|
@@ -635,22 +767,67 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
635
767
|
</div>
|
|
636
768
|
);
|
|
637
769
|
|
|
638
|
-
const integrationRow = (name: string, installed: boolean, extra: any, installTarget: string, uninstallTarget: string, tips: any) => (
|
|
639
|
-
<div style={S.card}>
|
|
640
|
-
<span style={{ fontWeight: 600, fontSize: 13, minWidth: 92 }}>{name}</span>
|
|
641
|
-
<span style={S.chip(installed)}>{installed ? `● ${copy.installed}` : `○ ${copy.notInstalled}`}</span>
|
|
642
|
-
{
|
|
770
|
+
const integrationRow = (name: string, installed: boolean, ready: boolean, extra: any, installTarget: string, uninstallTarget: string, tips: any) => (
|
|
771
|
+
<div style={S.card}>
|
|
772
|
+
<span style={{ fontWeight: 600, fontSize: 13, minWidth: 92 }}>{name}</span>
|
|
773
|
+
<span style={S.chip(installed)}>{installed ? `● ${copy.installed}` : `○ ${copy.notInstalled}`}</span>
|
|
774
|
+
<span style={S.chip(ready)}>{ready ? `● ${copy.ready}` : `○ ${copy.notReady}`}</span>
|
|
775
|
+
{extra}
|
|
643
776
|
<span style={{ flex: 1 }} />
|
|
644
777
|
{!installed && <button style={S.btn} title={tips.install} disabled={busy} onClick={() => { void act(installTarget); }}>{copy.install}</button>}
|
|
645
778
|
{installed && <button style={S.btn} title={tips.update} disabled={busy} onClick={() => { void act(installTarget); }}>{copy.update}</button>}
|
|
646
779
|
{installed && <button style={{ ...S.btn, opacity: 0.75 }} title={tips.restore} disabled={busy} onClick={() => { void act(uninstallTarget, name); }}>{copy.restore}</button>}
|
|
647
780
|
</div>
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const adaptive = normalizeAdaptive(config?.worker?.model_policy?.adaptive);
|
|
784
|
+
const setAdaptiveLocal = (candidate: AdaptiveConfig) => {
|
|
785
|
+
const next = normalizeAdaptive(candidate);
|
|
786
|
+
setConfig((current: any) => ({
|
|
787
|
+
...current,
|
|
788
|
+
worker: {
|
|
789
|
+
...current.worker,
|
|
790
|
+
model_policy: { ...current.worker?.model_policy, adaptive: next },
|
|
791
|
+
},
|
|
792
|
+
}));
|
|
793
|
+
return next;
|
|
794
|
+
};
|
|
795
|
+
const saveAdaptive = (candidate: AdaptiveConfig) => {
|
|
796
|
+
const next = setAdaptiveLocal(candidate);
|
|
797
|
+
void applyPatch({ worker: { model_policy: { adaptive: next } } });
|
|
798
|
+
};
|
|
799
|
+
const modelActivity = aggregateModelInvocations(jobs);
|
|
800
|
+
const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
|
|
801
|
+
const hostReadiness = projectHostReadiness({ installStatus: status, runtime: runtimeInfo, surface });
|
|
802
|
+
|
|
803
|
+
if (!currentSurfaceResponsibilities.fullControlPlane) {
|
|
804
|
+
return <MinimalCrewPanel locale={locale} surface={surface} runtime={runtimeInfo} />;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return (
|
|
808
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 13, lineHeight: 1.55 }}>
|
|
809
|
+
<div style={{
|
|
810
|
+
border: '1px solid rgba(128,128,128,0.24)', borderRadius: 12, padding: '14px 15px',
|
|
811
|
+
display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap',
|
|
812
|
+
background: 'linear-gradient(135deg, rgba(74,158,255,0.10), rgba(128,128,128,0.025) 56%)',
|
|
813
|
+
}}>
|
|
814
|
+
<div style={{ minWidth: 240, flex: 1 }}>
|
|
815
|
+
<div style={{ fontSize: 20, fontWeight: 680, letterSpacing: '-0.01em' }}>{copy.title}</div>
|
|
816
|
+
<div style={{ opacity: 0.7, fontSize: 12.5, marginTop: 2 }}>{copy.intro}</div>
|
|
817
|
+
<div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
|
|
818
|
+
<span style={S.chip(!!status?.codex?.ready)}>Codex {status?.codex?.ready ? 'READY' : 'CHECK'}</span>
|
|
819
|
+
<span style={S.chip(!!status?.claude?.ready)}>Claude {status?.claude?.ready ? 'READY' : 'CHECK'}</span>
|
|
820
|
+
<span style={S.chip(jobs.some((job) => job.status === 'running'))}>{jobs.filter((job) => job.status === 'running').length} running</span>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 3 }}>
|
|
824
|
+
<a href={CREW_HARNESS_URL} target="_blank" rel="noopener noreferrer" style={{
|
|
825
|
+
...S.btn, textDecoration: 'none', fontWeight: 650, padding: '7px 12px',
|
|
826
|
+
borderColor: 'rgba(74,158,255,0.55)', background: 'rgba(74,158,255,0.10)',
|
|
827
|
+
}}>{copy.openHarness}</a>
|
|
828
|
+
<span style={{ fontSize: 10.5, opacity: 0.52 }}>{copy.harnessHint}</span>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
654
831
|
|
|
655
832
|
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 5 }}>
|
|
656
833
|
<div style={{ ...S.section, margin: 0, flex: 1 }}>{copy.globalConfig}</div>
|
|
@@ -658,16 +835,33 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
658
835
|
<button type="button" style={{ ...S.btn, padding: '3px 9px' }} onClick={() => toggleAllSections(false)}>{copy.collapseAll}</button>
|
|
659
836
|
</div>
|
|
660
837
|
|
|
661
|
-
<CollapsibleSection sectionId="integrations" title={copy.sectionNames.integrations}
|
|
662
|
-
summary={sectionSummary(status?.claude?.
|
|
663
|
-
expanded={!!expandedSections.integrations} onToggle={() => toggleSection('integrations')}>
|
|
664
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
665
|
-
{
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
838
|
+
<CollapsibleSection sectionId="integrations" title={copy.sectionNames.integrations}
|
|
839
|
+
summary={sectionSummary(status?.claude?.ready ? 'Claude READY' : 'Claude CHECK', status?.codex?.ready ? 'Codex READY' : 'Codex CHECK')}
|
|
840
|
+
expanded={!!expandedSections.integrations} onToggle={() => toggleSection('integrations')}>
|
|
841
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
842
|
+
<div style={S.block}>
|
|
843
|
+
<div>
|
|
844
|
+
<div style={S.settingTitle}>{copy.hostReadiness}</div>
|
|
845
|
+
<div style={S.settingDesc}>{copy.hostReadinessHint}</div>
|
|
846
|
+
</div>
|
|
847
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px 10px' }}>
|
|
848
|
+
{hostReadiness.map((row) => (
|
|
849
|
+
<div key={row.id} style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 0, padding: '4px 7px', border: '1px solid rgba(128,128,128,0.16)', borderRadius: 7 }}>
|
|
850
|
+
<span style={{ flex: 1, minWidth: 0, fontSize: 11.5 }}>{(copy as any).readinessLabels[row.id]}</span>
|
|
851
|
+
{row.detail && <span style={{ ...S.mono, opacity: 0.55 }}>{row.detail}</span>}
|
|
852
|
+
<span style={readinessChip(row.state)}>{(copy as any).readinessStates[row.state]}</span>
|
|
853
|
+
</div>
|
|
854
|
+
))}
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
{integrationRow('Claude Code', !!status?.claude?.installed, !!status?.claude?.ready,
|
|
858
|
+
status?.claude?.installed ? <span style={S.chip(!!status?.claude?.hud)}>{copy.hud}</span> : null,
|
|
859
|
+
'claude', 'claude-uninstall', (copy as any).tips.claude)}
|
|
860
|
+
{integrationRow('Codex', !!status?.codex?.installed, !!status?.codex?.ready,
|
|
861
|
+
status?.codex?.ready ? null : <span title={(status?.codex?.missing ?? []).join(', ')} style={{ fontSize: 11, opacity: 0.6 }}>{(status?.codex?.missing ?? []).join(' · ')}</span>,
|
|
862
|
+
'codex', 'codex-uninstall', (copy as any).tips.codex)}
|
|
863
|
+
</div>
|
|
864
|
+
</CollapsibleSection>
|
|
671
865
|
|
|
672
866
|
{config && (<>
|
|
673
867
|
{(() => {
|
|
@@ -693,7 +887,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
693
887
|
const savePriority = (tier: string, list: any[]) => {
|
|
694
888
|
const key = `${tier}_model_priority`;
|
|
695
889
|
setConfig((current: any) => ({ ...current, [key]: list, [`${tier}_model_priority_configured`]: true }));
|
|
696
|
-
|
|
890
|
+
void applyPatch({ [key]: list });
|
|
697
891
|
};
|
|
698
892
|
const modelMeta = (ref: any) => {
|
|
699
893
|
const provider = catalogProviders.find((item) => item.id === ref.provider);
|
|
@@ -905,9 +1099,30 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
905
1099
|
<label style={S.field}><span style={S.fieldLabel}>{copy.presetPro}</span>
|
|
906
1100
|
<CustomSelect value={config.preset_pro ?? 'default'} onChange={(v) => field('preset_pro', v)} options={presetOptions} /></label>
|
|
907
1101
|
</>))}
|
|
908
|
-
</CollapsibleSection>
|
|
909
|
-
|
|
910
|
-
<CollapsibleSection sectionId="
|
|
1102
|
+
</CollapsibleSection>
|
|
1103
|
+
|
|
1104
|
+
<CollapsibleSection sectionId="adaptive" title={copy.sectionNames.adaptive}
|
|
1105
|
+
summary={sectionSummary(adaptive.enabled ? copy.adaptiveEnabled : 'off', `${adaptive.min_samples}/${adaptive.window_size}`, copy.adaptiveBoundary)}
|
|
1106
|
+
expanded={!!expandedSections.adaptive} onToggle={() => toggleSection('adaptive')}>
|
|
1107
|
+
{block({ t: copy.adaptiveTitle, d: copy.adaptiveHint }, (<>
|
|
1108
|
+
<label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 7, gridColumn: '1 / -1' }}>
|
|
1109
|
+
<input type="checkbox" checked={adaptive.enabled}
|
|
1110
|
+
onChange={(event) => saveAdaptive({ ...adaptive, enabled: event.target.checked })} />
|
|
1111
|
+
<span style={{ fontSize: 12.5 }}>{copy.adaptiveEnabled}</span>
|
|
1112
|
+
<span style={{ fontSize: 11, opacity: 0.55 }}>· {copy.adaptiveBoundary}</span>
|
|
1113
|
+
</label>
|
|
1114
|
+
<label style={S.field}><span style={S.fieldLabel}>{copy.adaptiveWindow}</span>
|
|
1115
|
+
<input type="number" min={1} max={32} value={adaptive.window_size} style={S.input}
|
|
1116
|
+
onChange={(event) => setAdaptiveLocal({ ...adaptive, window_size: clampInt(event.target.value, adaptive.window_size, 1, 32) })}
|
|
1117
|
+
onBlur={() => saveAdaptive(adaptive)} /></label>
|
|
1118
|
+
<label style={S.field}><span style={S.fieldLabel}>{copy.adaptiveSamples}</span>
|
|
1119
|
+
<input type="number" min={1} max={adaptive.window_size} value={adaptive.min_samples} style={S.input}
|
|
1120
|
+
onChange={(event) => setAdaptiveLocal({ ...adaptive, min_samples: clampInt(event.target.value, adaptive.min_samples, 1, adaptive.window_size) })}
|
|
1121
|
+
onBlur={() => saveAdaptive(adaptive)} /></label>
|
|
1122
|
+
</>))}
|
|
1123
|
+
</CollapsibleSection>
|
|
1124
|
+
|
|
1125
|
+
<CollapsibleSection sectionId="runtime" title={copy.sectionNames.runtime}
|
|
911
1126
|
summary={sectionSummary(config.mode, `${config.default_timeout_seconds}s`, config.hub_url)}
|
|
912
1127
|
expanded={!!expandedSections.runtime} onToggle={() => toggleSection('runtime')}>
|
|
913
1128
|
{block(copy.cardRuntime, (<>
|
|
@@ -917,14 +1132,15 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
917
1132
|
<label style={S.field}><span style={S.fieldLabel}>{copy.timeout}</span>
|
|
918
1133
|
<input style={{ ...S.input, width: '100%', boxSizing: 'border-box' as const }} type="number" min={60} max={7200} value={config.default_timeout_seconds}
|
|
919
1134
|
onChange={(e) => fieldLocal('default_timeout_seconds', Number(e.target.value))}
|
|
920
|
-
onBlur={() => applyPatch({ default_timeout_seconds: config.default_timeout_seconds })} /></label>
|
|
1135
|
+
onBlur={() => { void applyPatch({ default_timeout_seconds: config.default_timeout_seconds }); }} /></label>
|
|
921
1136
|
<label style={S.field}><span style={S.fieldLabel}>{copy.hubUrl}</span>
|
|
922
1137
|
<input style={{ ...S.input, width: '100%', boxSizing: 'border-box' as const }} value={config.hub_url}
|
|
923
1138
|
onChange={(e) => fieldLocal('hub_url', e.target.value)}
|
|
924
|
-
onBlur={() => applyPatch({ hub_url: config.hub_url })}
|
|
1139
|
+
onBlur={() => { void applyPatch({ hub_url: config.hub_url }); }}
|
|
925
1140
|
onKeyDown={(e: any) => { if (e.key === 'Enter') e.currentTarget.blur(); }} /></label>
|
|
926
|
-
</>))}
|
|
927
|
-
|
|
1141
|
+
</>))}
|
|
1142
|
+
<ActivationSummary activation={config.config_activation} locale={locale} />
|
|
1143
|
+
</CollapsibleSection>
|
|
928
1144
|
|
|
929
1145
|
<CollapsibleSection sectionId="multimodal" title={copy.sectionNames.multimodal}
|
|
930
1146
|
summary={sectionSummary(config.vision_enabled ? `${copy.capVision}: ${config.vision_provider}` : `${copy.capVision}: off`, config.imagegen_enabled ? `${copy.capImagegen}: ${config.imagegen_provider}` : `${copy.capImagegen}: off`)}
|
|
@@ -942,7 +1158,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
942
1158
|
const m = custom ? (custom.models?.[0] ?? 'default') : (VISION_MODELS[p] ?? ['default'])[0];
|
|
943
1159
|
setModelAdd(null);
|
|
944
1160
|
setConfig((c: any) => ({ ...c, vision_provider: p, vision_model: m }));
|
|
945
|
-
|
|
1161
|
+
void applyPatch({ vision_provider: p, vision_model: m });
|
|
946
1162
|
if ((p === 'grok' || p === 'agy') && !dynModels[p]) {
|
|
947
1163
|
void get(`/vision-models?provider=${p}`).then((r) => {
|
|
948
1164
|
if (r.ok) setDynModels((d) => ({ ...d, [p]: r.models }));
|
|
@@ -1104,58 +1320,80 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1104
1320
|
<CollapsibleSection sectionId="jobs" title={copy.sectionNames.jobs}
|
|
1105
1321
|
summary={sectionSummary(copy.jobCount(jobs.length), jobs.some((job) => job.status === 'running') && copy.runningCount(jobs.filter((job) => job.status === 'running').length))}
|
|
1106
1322
|
expanded={!!expandedSections.jobs} onToggle={() => toggleSection('jobs')}>
|
|
1107
|
-
{jobs.length === 0 ? (
|
|
1108
|
-
<div style={{ opacity: 0.55 }}>{copy.empty}</div>
|
|
1109
|
-
) : (
|
|
1110
|
-
<div style={{ overflowX: 'auto' }}>
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
bottom: window.innerHeight - rect.top + 4,
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1323
|
+
{jobs.length === 0 ? (
|
|
1324
|
+
<div style={{ opacity: 0.55 }}>{copy.empty}</div>
|
|
1325
|
+
) : (
|
|
1326
|
+
<div style={{ overflowX: 'auto', border: '1px solid rgba(128,128,128,0.20)', borderRadius: 8, padding: '0 9px' }}>
|
|
1327
|
+
<table style={{ borderCollapse: 'collapse', width: '100%', tableLayout: 'fixed', minWidth: 1020 }}>
|
|
1328
|
+
<colgroup>
|
|
1329
|
+
<col style={{ width: 68 }} /><col style={{ width: 72 }} /><col style={{ width: 58 }} />
|
|
1330
|
+
<col style={{ width: 190 }} /><col style={{ width: 70 }} /><col style={{ width: 70 }} />
|
|
1331
|
+
<col style={{ width: 105 }} /><col style={{ width: 84 }} /><col style={{ width: 300 }} />
|
|
1332
|
+
</colgroup>
|
|
1333
|
+
<thead><tr>
|
|
1334
|
+
{[copy.col.id, copy.col.role, copy.col.source, copy.col.model, copy.col.tier, copy.col.status, copy.col.progress, copy.col.tokens, copy.col.task].map((heading) => (
|
|
1335
|
+
<th key={heading} style={{ ...S.tight, fontSize: 10.5, opacity: 0.55, fontWeight: 500 }}>{heading}</th>
|
|
1336
|
+
))}
|
|
1337
|
+
</tr></thead>
|
|
1338
|
+
<tbody>
|
|
1339
|
+
{jobs.map((job) => {
|
|
1340
|
+
const color = job.status === 'running' ? '#4a9eff' : job.status === 'done' ? '#3fb950' : '#f85149';
|
|
1341
|
+
return (
|
|
1342
|
+
<tr key={job.id}>
|
|
1343
|
+
<td style={{ ...S.tight, ...S.mono, fontWeight: 600 }} title={job.id}>{String(job.id).replace(/-[a-z0-9]+$/, '')}</td>
|
|
1344
|
+
<td style={S.tight}><span style={S.chip(job.role === 'reviewer')}>{job.role === 'reviewer' ? 'Reviewer' : 'Worker'}</span></td>
|
|
1345
|
+
<td style={S.tight}><span style={S.chip(job.source === 'claude-code' || job.source === 'codex')}>{sourceLabel(job.source)}</span></td>
|
|
1346
|
+
<td style={{ ...S.tight, ...S.mono }} title={`${job.provider ?? '—'} / ${job.model ?? '—'}`}>{job.provider ?? '—'} / {job.model ?? '—'}</td>
|
|
1347
|
+
<td style={S.tight}>{job.tier}/{job.effort}</td>
|
|
1348
|
+
<td style={S.tight} title={`${job.status}${job.currentTool ? ` · ${job.currentTool}` : ''}`}>
|
|
1349
|
+
<span style={{ color }} aria-hidden="true">●</span> <span style={{ color, fontSize: 10.5 }}>{job.status}</span>
|
|
1350
|
+
</td>
|
|
1351
|
+
<td style={{ ...S.tight, ...S.mono }} title={copy.progressTip(elapsed(job.startedAt, job.endedAt), job.turn, job.step, job.toolCalls ?? '-')}>{elapsed(job.startedAt, job.endedAt)} · {job.turn}.{job.step} · {job.toolCalls ?? '-'}t</td>
|
|
1352
|
+
<td style={{ ...S.tight, ...S.mono }}>{job.tokens ? `${ktok(job.tokens.input)}/${ktok(job.tokens.output)}` : '—'}</td>
|
|
1353
|
+
<td
|
|
1354
|
+
style={{ ...S.cell, position: 'relative' }}
|
|
1355
|
+
onMouseEnter={(event: any) => {
|
|
1356
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1357
|
+
setHoverTask({ id: job.id, text: job.task, right: Math.max(8, window.innerWidth - rect.right), top: rect.bottom + 4, bottom: window.innerHeight - rect.top + 4, flip: rect.top < 240 });
|
|
1358
|
+
}}
|
|
1359
|
+
onMouseLeave={() => setHoverTask(null)}>
|
|
1360
|
+
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: 1.45, fontSize: 12 }}>
|
|
1361
|
+
{job.task}
|
|
1362
|
+
</div>
|
|
1363
|
+
<div style={{ fontSize: 10, opacity: 0.5 }}>{job.selection_source ?? 'unknown'}</div>
|
|
1364
|
+
</td>
|
|
1365
|
+
</tr>
|
|
1366
|
+
);
|
|
1367
|
+
})}
|
|
1368
|
+
</tbody>
|
|
1369
|
+
</table>
|
|
1370
|
+
</div>
|
|
1371
|
+
)}
|
|
1372
|
+
|
|
1373
|
+
<div style={{ ...S.block, marginTop: 2 }}>
|
|
1374
|
+
<div>
|
|
1375
|
+
<div style={S.settingTitle}>{copy.modelActivity}</div>
|
|
1376
|
+
<div style={S.settingDesc}>{copy.modelActivityHint}</div>
|
|
1377
|
+
</div>
|
|
1378
|
+
{modelActivity.length === 0 ? <div style={{ opacity: 0.55 }}>{copy.noModelActivity}</div> : (
|
|
1379
|
+
<div style={{ overflowX: 'auto' }}>
|
|
1380
|
+
<div role="table" style={{ minWidth: 620 }}>
|
|
1381
|
+
<div role="row" style={{ display: 'grid', gridTemplateColumns: 'minmax(190px, 1.35fr) 72px minmax(170px, 1fr) 125px', gap: 10, padding: '0 6px 5px', fontSize: 10.5, opacity: 0.55 }}>
|
|
1382
|
+
<span role="columnheader">{copy.model}</span><span role="columnheader">{copy.calls}</span><span role="columnheader">{copy.invocationSource} / {copy.routingSource}</span><span role="columnheader">{copy.lastCalled}</span>
|
|
1383
|
+
</div>
|
|
1384
|
+
{modelActivity.map((entry) => (
|
|
1385
|
+
<div role="row" key={`${entry.provider}/${entry.model}`} style={{ display: 'grid', gridTemplateColumns: 'minmax(190px, 1.35fr) 72px minmax(170px, 1fr) 125px', gap: 10, alignItems: 'center', padding: '7px 6px', borderTop: '1px solid rgba(128,128,128,0.16)' }}>
|
|
1386
|
+
<span role="cell" style={{ minWidth: 0 }}><strong style={{ display: 'block', fontSize: 12 }}>{entry.model}</strong><span style={{ ...S.mono, opacity: 0.55 }}>{entry.provider}</span></span>
|
|
1387
|
+
<span role="cell" style={{ ...S.mono, fontSize: 15, fontWeight: 650 }}>{entry.count}</span>
|
|
1388
|
+
<span role="cell" style={{ minWidth: 0, fontSize: 11.5 }}><span style={{ display: 'block' }}>{entry.task_sources.map(sourceLabel).join(' · ')}</span><span style={{ display: 'block', opacity: 0.55 }}>{entry.selection_sources.join(' · ')} · {entry.roles.join(' / ')}</span></span>
|
|
1389
|
+
<span role="cell" style={{ ...S.mono, fontSize: 10.5 }}>{formatTimestamp(entry.last_called_at, locale)}</span>
|
|
1390
|
+
</div>
|
|
1391
|
+
))}
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
)}
|
|
1395
|
+
</div>
|
|
1396
|
+
</CollapsibleSection>
|
|
1159
1397
|
{hoverTask && createPortal(
|
|
1160
1398
|
<div style={{
|
|
1161
1399
|
position: 'fixed', right: hoverTask.right, zIndex: 9999,
|
|
@@ -1173,8 +1411,9 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1173
1411
|
);
|
|
1174
1412
|
}
|
|
1175
1413
|
|
|
1176
|
-
export function apply(ctx: any): void {
|
|
1177
|
-
let runningCount = 0;
|
|
1414
|
+
export function apply(ctx: any): void {
|
|
1415
|
+
let runningCount = 0;
|
|
1416
|
+
let officialSurface: boolean | null = null;
|
|
1178
1417
|
ctx.slots.inject('settings.section', () => {
|
|
1179
1418
|
let dispose = register();
|
|
1180
1419
|
function register() {
|
|
@@ -1195,10 +1434,16 @@ export function apply(ctx: any): void {
|
|
|
1195
1434
|
() => <WorkersPanel ctx={ctx} />,
|
|
1196
1435
|
);
|
|
1197
1436
|
}
|
|
1198
|
-
const poll = async () => {
|
|
1199
|
-
if (document.visibilityState !== 'visible') return;
|
|
1200
|
-
try {
|
|
1201
|
-
|
|
1437
|
+
const poll = async () => {
|
|
1438
|
+
if (document.visibilityState !== 'visible') return;
|
|
1439
|
+
try {
|
|
1440
|
+
if (officialSurface === null) {
|
|
1441
|
+
const response = await fetch(`${API}/bridge-status`, { cache: 'no-store' });
|
|
1442
|
+
const bridgeStatus = response.ok ? await response.json() : null;
|
|
1443
|
+
officialSurface = classifyCrewSurface({ bridgeStatus }) === CREW_UI_SURFACES.OFFICIAL;
|
|
1444
|
+
}
|
|
1445
|
+
if (!officialSurface) return;
|
|
1446
|
+
const r = await (await fetch(`${API}/jobs`, { cache: 'no-store' })).json();
|
|
1202
1447
|
const n = r.ok ? (r.jobs ?? []).filter((j: any) => j.status === 'running').length : 0;
|
|
1203
1448
|
if (n !== runningCount) {
|
|
1204
1449
|
runningCount = n;
|