@ran-sh/dsh-crew 1.1.1 → 1.2.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -17
- package/.claude-plugin/plugin.json +7 -1
- package/README.md +44 -15
- package/README.zh.md +37 -12
- package/agents/ds-flash.md +16 -18
- package/agents/ds-pro.md +16 -24
- package/agents/ds-reviewer.md +15 -15
- package/agents/ds-worker.md +16 -16
- package/bin/dsh-crew.mjs +16 -12
- package/codex/AGENTS.md +37 -85
- package/codex/agents/ds-flash.toml +18 -14
- package/codex/agents/ds-pro.toml +18 -15
- package/codex/agents/ds-reviewer.toml +17 -13
- package/codex/agents/ds-worker.toml +17 -13
- package/codex/prompts/dsh-config.md +17 -1
- package/codex/prompts/dsh-status.md +5 -1
- package/commands/config.md +16 -4
- package/commands/status.md +6 -2
- package/docs/installation.md +54 -9
- package/docs/readiness-matrix.md +14 -6
- package/docs/ui-surfaces.md +32 -17
- package/lib/client.js +769 -177
- package/official-web-bridge/lib/client.js +297 -209
- package/official-web-bridge/overlay-entry.mjs +59 -0
- package/package.json +6 -3
- package/scripts/build-client.mjs +2 -0
- package/scripts/setup.mjs +8 -7
- package/scripts/verify-crew-ui-polish.mjs +145 -0
- package/scripts/verify-history-ui.mjs +97 -0
- package/scripts/verify-npm-install.mjs +1 -1
- package/src/client/history-panel.tsx +108 -0
- package/src/client/host-readiness.mjs +15 -6
- package/src/client/index.tsx +91 -69
- package/src/client/model-callability-view.mjs +17 -0
- package/src/client/panel-chrome.tsx +40 -0
- package/src/client/quick-panel.tsx +88 -73
- package/src/client/readiness-envelope.mjs +34 -0
- package/src/client/task-telemetry.mjs +11 -4
- package/src/config-readiness.mjs +181 -115
- package/src/extension-contract.mjs +44 -26
- package/src/history/admission-gate.mjs +51 -0
- package/src/history/archive-store.mjs +261 -0
- package/src/history/cleanup-plan.mjs +64 -0
- package/src/history/http.mjs +32 -0
- package/src/history/operation.mjs +86 -0
- package/src/history/runner.mjs +51 -0
- package/src/history/runtime.mjs +28 -0
- package/src/history/service.mjs +137 -0
- package/src/history/state.mjs +27 -0
- package/src/hub/index.mjs +141 -24
- package/src/hub-client.mjs +17 -15
- package/src/image-output.mjs +71 -0
- package/src/install/install-legacy.mjs +126 -23
- package/src/install/npx-lifecycle.mjs +373 -64
- package/src/install/official-frontend-assets.mjs +78 -0
- package/src/install/payload-content.mjs +88 -0
- package/src/install/windows-startup.mjs +107 -19
- package/src/install/windows-supervisor-adapter.mjs +443 -0
- package/src/install/windows-supervisor-lifecycle.mjs +782 -0
- package/src/install/zcode.mjs +53 -14
- package/src/job-contracts.mjs +37 -31
- package/src/jobs.mjs +2 -2
- package/src/mcp-runtime.mjs +74 -9
- package/src/model-callability-contract.mjs +79 -0
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +0 -2
- package/src/provider-health.mjs +130 -124
- package/src/readiness-matrix.mjs +20 -15
- package/src/runtime-identity-contract.mjs +34 -0
- package/src/runtime-identity.mjs +14 -4
- package/src/runtime-readiness-snapshot.mjs +285 -87
- package/src/server.mjs +86 -76
- package/src/standalone-sdk.mjs +23 -0
- package/src/supervisor/restart-request.mjs +64 -10
- package/src/workflow-runtime.mjs +28 -13
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +327 -51
- package/windows/supervisor-control.ps1 +467 -0
- package/zcode/AGENTS.md +4 -1
- package/zcode/agents/ds-reviewer.md +31 -36
- package/zcode/agents/ds-worker.md +31 -39
- package/zcode/commands/dsh-config.md +17 -6
- package/zcode/commands/dsh-status.md +5 -4
- package/.mcp.json +0 -8
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { PanelHeader, PanelStyles } from './panel-chrome';
|
|
2
3
|
|
|
3
4
|
// DSH Crew QUICK CONTROLS panel for the official 3080 surface.
|
|
4
5
|
//
|
|
5
6
|
// Narrow by design: only the user-facing master switch, flash/pro model
|
|
6
7
|
// priority lists and vision/imagegen toggles — all other operations belong
|
|
7
8
|
// to the native 3210 full control plane. Talks only to the quick endpoints
|
|
8
|
-
// (/_dsh/dsh-crew/quick-config, /quick-status
|
|
9
|
-
// /runtime/restart-status). NEVER writes to any other surface.
|
|
9
|
+
// (/_dsh/dsh-crew/quick-config, /quick-status). Runtime maintenance stays on 3210.
|
|
10
10
|
|
|
11
11
|
const API = '/_dsh/dsh-crew';
|
|
12
12
|
const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3210/';
|
|
@@ -33,14 +33,20 @@ const RESTART_KEYS = new Set([
|
|
|
33
33
|
const T = {
|
|
34
34
|
zh: {
|
|
35
35
|
title: 'DSH Crew 快捷控制',
|
|
36
|
-
openFull: '
|
|
37
|
-
running: '
|
|
36
|
+
openFull: '打开 3210 后台 →',
|
|
37
|
+
running: '后台已连接',
|
|
38
|
+
surface: '3080 / 日常控制',
|
|
39
|
+
description: '常用开关与模型顺序在这里调整,更多设置进入后台。',
|
|
40
|
+
priorityHint: '从上到下按优先级排列;从列表移除不会删除 Provider。',
|
|
41
|
+
empty: '未配置优先模型,按现有后备策略选择。',
|
|
42
|
+
models: (n: number) => `${n} 个模型`,
|
|
43
|
+
moveUp: '上移', moveDown: '下移', remove: '从优先级移除',
|
|
38
44
|
unavailable: 'Crew 后端不可用',
|
|
39
45
|
openDiag: '打开诊断',
|
|
40
46
|
crew: 'Crew',
|
|
41
47
|
enabled: '启用子 Agent',
|
|
42
|
-
flash: 'Flash
|
|
43
|
-
pro: 'Pro
|
|
48
|
+
flash: 'Worker / Flash',
|
|
49
|
+
pro: 'Reviewer / Pro',
|
|
44
50
|
addModel: '+ 添加模型',
|
|
45
51
|
multimodal: '多模态',
|
|
46
52
|
vision: '视觉',
|
|
@@ -55,14 +61,20 @@ const T = {
|
|
|
55
61
|
},
|
|
56
62
|
en: {
|
|
57
63
|
title: 'DSH Crew Quick Controls',
|
|
58
|
-
openFull: 'Open
|
|
59
|
-
running: '
|
|
64
|
+
openFull: 'Open Crew backend (3210) →',
|
|
65
|
+
running: 'Backend connected',
|
|
66
|
+
surface: '3080 / DAILY CONTROLS',
|
|
67
|
+
description: 'Manage everyday switches and model order here. Open the backend for advanced settings.',
|
|
68
|
+
priorityHint: 'Ordered by priority. Removing a model here does not delete its provider.',
|
|
69
|
+
empty: 'No priority models. The existing fallback policy applies.',
|
|
70
|
+
models: (n: number) => `${n} models`,
|
|
71
|
+
moveUp: 'Move up', moveDown: 'Move down', remove: 'Remove from priority',
|
|
60
72
|
unavailable: 'Crew backend unavailable',
|
|
61
73
|
openDiag: 'Open diagnostics',
|
|
62
74
|
crew: 'Crew',
|
|
63
75
|
enabled: 'Enable sub-agents',
|
|
64
|
-
flash: 'Flash
|
|
65
|
-
pro: 'Pro
|
|
76
|
+
flash: 'Worker / Flash',
|
|
77
|
+
pro: 'Reviewer / Pro',
|
|
66
78
|
addModel: '+ Add model',
|
|
67
79
|
multimodal: 'Multimodal',
|
|
68
80
|
vision: 'Vision',
|
|
@@ -73,23 +85,39 @@ const T = {
|
|
|
73
85
|
saved: 'Saved',
|
|
74
86
|
working: 'Working…',
|
|
75
87
|
providerPlaceholder: 'provider',
|
|
76
|
-
modelPlaceholder: '
|
|
88
|
+
modelPlaceholder: 'Model',
|
|
77
89
|
},
|
|
78
90
|
};
|
|
79
91
|
|
|
80
92
|
function LocalStyles() {
|
|
81
93
|
return (
|
|
82
94
|
<style>{`
|
|
83
|
-
.crew-quick-card {
|
|
95
|
+
.dsh-crew-ui.crew-quick-card { display: flex; flex-direction: column; gap: 12px; }
|
|
84
96
|
.crew-quick-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
85
97
|
.crew-quick-title { font-size: 16px; font-weight: 680; }
|
|
86
98
|
.crew-quick-section { font-weight: 650; opacity: 0.85; }
|
|
87
99
|
.crew-quick-chip { border: 1px solid rgba(128,128,128,0.35); border-radius: 999px; padding: 1px 10px; font-size: 12px; }
|
|
88
|
-
.crew-quick-btn { border: 1px solid
|
|
100
|
+
.crew-quick-card .crew-quick-btn { border: 1px solid var(--crew-line); border-radius: 7px; min-height: 30px; padding: 3px 9px; cursor: pointer; background: transparent; color: inherit; font-size: 12px; }
|
|
89
101
|
.crew-quick-btn.primary { border-color: #4a9eff; color: #4a9eff; font-weight: 650; }
|
|
90
|
-
.crew-quick-input { border: 1px solid
|
|
102
|
+
.crew-quick-card .crew-quick-input { border: 1px solid var(--crew-line); border-radius: 6px; padding: 6px 9px; font-size: 12px; min-width: 0; width: 100%; background: transparent; color: inherit; }
|
|
91
103
|
.crew-quick-notice { opacity: 0.75; font-size: 12.5px; }
|
|
92
|
-
.crew-quick-
|
|
104
|
+
.crew-quick-card .crew-quick-master { padding: 11px 14px; border: 1px solid var(--crew-line); border-radius: 10px; justify-content: space-between; }
|
|
105
|
+
.crew-quick-card input[type=checkbox] { accent-color: var(--crew-accent); width: 15px; height: 15px; }
|
|
106
|
+
.crew-quick-card label { display: inline-flex; gap: 7px; align-items: center; }
|
|
107
|
+
.crew-quick-card .crew-quick-group { border: 1px solid var(--crew-line); border-radius: 10px; overflow: hidden; }
|
|
108
|
+
.crew-quick-card .crew-quick-group > summary { cursor: pointer; padding: 12px 14px; }
|
|
109
|
+
.crew-quick-card .crew-quick-count { margin-left: 10px; font-size: 11px; opacity: .65; font-weight: 400; }
|
|
110
|
+
.crew-quick-card .crew-quick-content { padding: 0 14px 14px; }
|
|
111
|
+
.crew-quick-card .crew-quick-list { padding: 0; margin: 10px 0; list-style: none; }
|
|
112
|
+
.crew-quick-card .crew-quick-model { display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: 9px; align-items: center; border-top: 1px solid var(--crew-line); padding: 10px 0; }
|
|
113
|
+
.crew-quick-card .crew-quick-rank { opacity: .5; font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
114
|
+
.crew-quick-card .crew-quick-model-name { font-size: 12.5px; font-weight: 600; overflow-wrap: anywhere; }
|
|
115
|
+
.crew-quick-card .crew-quick-provider { font-size: 11px; opacity: .65; overflow-wrap: anywhere; }
|
|
116
|
+
.crew-quick-card .crew-quick-actions { display: flex; gap: 4px; }
|
|
117
|
+
.crew-quick-card .crew-quick-add > summary { color: var(--crew-accent); cursor: pointer; font-size: 12px; padding: 4px 0; }
|
|
118
|
+
.crew-quick-card .crew-quick-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 7px; margin-top: 9px; }
|
|
119
|
+
.crew-quick-card .crew-quick-media { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 10px; align-items: center; margin-top: 10px; }
|
|
120
|
+
@media (max-width: 640px) { .crew-quick-card .crew-quick-form { grid-template-columns: minmax(0, 1fr); } .crew-quick-card .crew-quick-model { grid-template-columns: 18px minmax(0, 1fr); } .crew-quick-card .crew-quick-actions { grid-column: 2; justify-content: flex-end; } }
|
|
93
121
|
`}</style>
|
|
94
122
|
);
|
|
95
123
|
}
|
|
@@ -174,108 +202,95 @@ export function QuickPanel({ ctx }: { ctx: any }) {
|
|
|
174
202
|
void patch({ [listKey]: list } as unknown as QuickConfig);
|
|
175
203
|
};
|
|
176
204
|
|
|
177
|
-
const applyRestart = useCallback(async () => {
|
|
178
|
-
setBusy(true); setNotice(t.working);
|
|
179
|
-
try {
|
|
180
|
-
const created = await readJson(await fetch(`${API}/runtime/restart-request`, {
|
|
181
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
182
|
-
body: JSON.stringify({ confirm: true, reason: 'quick panel restart' }),
|
|
183
|
-
}));
|
|
184
|
-
if (!created.ok) throw new Error(created.code ?? created.error ?? 'restart failed');
|
|
185
|
-
const requestId = created.request_id;
|
|
186
|
-
const deadline = Date.now() + 90_000;
|
|
187
|
-
for (;;) {
|
|
188
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
189
|
-
if (Date.now() > deadline) throw new Error('restart timed out');
|
|
190
|
-
const status = await readJson(await fetch(`${API}/runtime/restart-status?id=${encodeURIComponent(requestId)}`, { cache: 'no-store' }));
|
|
191
|
-
if (!status.ok) continue;
|
|
192
|
-
if (status.state === 'VERIFIED') { setRestartPending(false); setNotice(t.saved); return; }
|
|
193
|
-
if (status.state !== 'RESTART_REQUESTED') throw new Error(status.state ?? 'restart failed');
|
|
194
|
-
}
|
|
195
|
-
} catch (e: any) {
|
|
196
|
-
setNotice(String(e?.message ?? e));
|
|
197
|
-
} finally { setBusy(false); }
|
|
198
|
-
}, [t]);
|
|
199
|
-
|
|
200
205
|
const modelList = (listKey: 'flash_model_priority' | 'pro_model_priority', label: string) => {
|
|
201
206
|
const list: ModelEntry[] = (config?.[listKey] as ModelEntry[] | undefined) ?? [];
|
|
202
207
|
const draft = drafts[listKey] ?? { provider: '', model: '' };
|
|
203
208
|
return (
|
|
204
|
-
<
|
|
205
|
-
<
|
|
209
|
+
<details className="crew-quick-group" open={listKey === 'flash_model_priority'}>
|
|
210
|
+
<summary><strong>{label}</strong><span className="crew-quick-count">{t.models(list.length)}</span></summary>
|
|
211
|
+
<div className="crew-quick-content">
|
|
212
|
+
<div className="crew-quick-notice">{t.priorityHint}</div>
|
|
213
|
+
{list.length === 0 && <p className="crew-quick-notice">{t.empty}</p>}
|
|
214
|
+
<ol className="crew-quick-list" aria-label={label}>
|
|
206
215
|
{list.map((entry, i) => (
|
|
207
|
-
<
|
|
208
|
-
<span>{i + 1}
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
<button className="crew-quick-btn" disabled={busy} onClick={() =>
|
|
212
|
-
|
|
216
|
+
<li key={`${entry.provider}/${entry.model}/${i}`} className="crew-quick-model">
|
|
217
|
+
<span className="crew-quick-rank" aria-hidden="true">{i + 1}.</span>
|
|
218
|
+
<div><div className="crew-quick-model-name">{entry.model}</div><div className="crew-quick-provider">{entry.provider}</div></div>
|
|
219
|
+
<div className="crew-quick-actions">
|
|
220
|
+
<button type="button" className="crew-quick-btn" aria-label={`${t.moveUp} ${entry.model}`} title={t.moveUp} disabled={busy || i === 0} onClick={() => moveModel(listKey, i, -1)}>↑</button>
|
|
221
|
+
<button type="button" className="crew-quick-btn" aria-label={`${t.moveDown} ${entry.model}`} title={t.moveDown} disabled={busy || i === list.length - 1} onClick={() => moveModel(listKey, i, 1)}>↓</button>
|
|
222
|
+
<button type="button" className="crew-quick-btn" aria-label={`${t.remove} ${entry.model}`} title={t.remove} disabled={busy} onClick={() => removeModel(listKey, i)}>×</button>
|
|
223
|
+
</div>
|
|
224
|
+
</li>
|
|
213
225
|
))}
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
</ol>
|
|
227
|
+
<details className="crew-quick-add">
|
|
228
|
+
<summary>{t.addModel}</summary>
|
|
229
|
+
<div className="crew-quick-form">
|
|
230
|
+
<input className="crew-quick-input" aria-label={`${label} Provider`} placeholder={t.providerPlaceholder} disabled={busy}
|
|
216
231
|
value={draft.provider} onChange={(e) => setDrafts((d) => ({ ...d, [listKey]: { ...draft, provider: e.target.value } }))} />
|
|
217
|
-
<input className="crew-quick-input" placeholder={t.modelPlaceholder} disabled={busy}
|
|
232
|
+
<input className="crew-quick-input" aria-label={`${label} ${locale === 'zh' ? '模型' : 'Model'}`} placeholder={t.modelPlaceholder} disabled={busy}
|
|
218
233
|
value={draft.model} onChange={(e) => setDrafts((d) => ({ ...d, [listKey]: { ...draft, model: e.target.value } }))} />
|
|
219
|
-
<button className="crew-quick-btn" disabled={busy} onClick={() => addModel(listKey)}>{t.addModel}</button>
|
|
234
|
+
<button type="button" className="crew-quick-btn" disabled={busy || !draft.provider.trim() || !draft.model.trim()} onClick={() => addModel(listKey)}>{t.addModel}</button>
|
|
220
235
|
</div>
|
|
221
|
-
|
|
236
|
+
</details>
|
|
237
|
+
</div>
|
|
238
|
+
</details>
|
|
222
239
|
);
|
|
223
240
|
};
|
|
224
241
|
|
|
225
242
|
if (ready === false) {
|
|
226
243
|
return (
|
|
227
|
-
<div className="crew-quick-card">
|
|
228
|
-
<LocalStyles />
|
|
244
|
+
<div className="dsh-crew-ui crew-quick-card">
|
|
245
|
+
<PanelStyles /><LocalStyles />
|
|
229
246
|
<div className="crew-quick-title">{t.title}</div>
|
|
230
247
|
<div className="crew-quick-notice">{t.unavailable}</div>
|
|
231
248
|
<div className="crew-quick-row">
|
|
232
|
-
<a href={FULL} target="_blank" rel="noreferrer">{t.openDiag}</a>
|
|
249
|
+
<a className="crew-nav-link" href={FULL} target="_blank" rel="noopener noreferrer">{t.openDiag}</a>
|
|
233
250
|
</div>
|
|
234
251
|
</div>
|
|
235
252
|
);
|
|
236
253
|
}
|
|
237
254
|
if (config === null) {
|
|
238
|
-
return (<div className="crew-quick-card"><LocalStyles /><div className="crew-quick-notice">{t.working}</div></div>);
|
|
255
|
+
return (<div className="dsh-crew-ui crew-quick-card"><PanelStyles /><LocalStyles /><div className="crew-quick-notice" role="status">{t.working}</div></div>);
|
|
239
256
|
}
|
|
240
257
|
|
|
241
258
|
return (
|
|
242
|
-
<div className="crew-quick-card">
|
|
243
|
-
<LocalStyles />
|
|
244
|
-
<
|
|
245
|
-
<span className="crew-quick-title">{t.title}</span>
|
|
259
|
+
<div className="dsh-crew-ui crew-quick-card">
|
|
260
|
+
<PanelStyles /><LocalStyles />
|
|
261
|
+
<PanelHeader title={t.title} eyebrow={t.surface} description={t.description} href={FULL} linkText={t.openFull}>
|
|
246
262
|
<span className="crew-quick-chip">{t.running} · 3210</span>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<div className="crew-quick-row">
|
|
263
|
+
</PanelHeader>
|
|
264
|
+
<div className="crew-quick-row crew-quick-master">
|
|
250
265
|
<span className="crew-quick-section">{t.crew}</span>
|
|
251
266
|
<label><input type="checkbox" disabled={busy} checked={config.subagents_enabled !== false}
|
|
252
267
|
onChange={(e) => toggle('subagents_enabled', e.target.checked)} /> {t.enabled}</label>
|
|
253
268
|
</div>
|
|
254
269
|
{modelList('flash_model_priority', t.flash)}
|
|
255
270
|
{modelList('pro_model_priority', t.pro)}
|
|
256
|
-
<
|
|
257
|
-
<
|
|
258
|
-
<div className="crew-quick-
|
|
271
|
+
<details className="crew-quick-group">
|
|
272
|
+
<summary><strong>{t.multimodal}</strong><span className="crew-quick-count">{t.vision} / {t.imagegen}</span></summary>
|
|
273
|
+
<div className="crew-quick-content">
|
|
274
|
+
<div className="crew-quick-media">
|
|
259
275
|
<label><input type="checkbox" disabled={busy} checked={config.vision_enabled === true}
|
|
260
276
|
onChange={(e) => toggle('vision_enabled', e.target.checked)} /> {t.vision}</label>
|
|
261
|
-
<
|
|
262
|
-
<input className="crew-quick-input" disabled={busy} defaultValue={config.vision_provider ?? ''} key={`vision-${config.vision_provider ?? ''}`}
|
|
277
|
+
<input className="crew-quick-input" aria-label={`${t.vision} Provider`} placeholder={t.provider} disabled={busy} defaultValue={config.vision_provider ?? ''} key={`vision-${config.vision_provider ?? ''}`}
|
|
263
278
|
onBlur={(e) => setProvider('vision_provider', e.target.value)} />
|
|
264
279
|
</div>
|
|
265
|
-
<div className="crew-quick-
|
|
280
|
+
<div className="crew-quick-media">
|
|
266
281
|
<label><input type="checkbox" disabled={busy} checked={config.imagegen_enabled === true}
|
|
267
282
|
onChange={(e) => toggle('imagegen_enabled', e.target.checked)} /> {t.imagegen}</label>
|
|
268
|
-
<
|
|
269
|
-
<input className="crew-quick-input" disabled={busy} defaultValue={config.imagegen_provider ?? ''}
|
|
283
|
+
<input className="crew-quick-input" aria-label={`${t.imagegen} Provider`} placeholder={t.provider} disabled={busy} defaultValue={config.imagegen_provider ?? ''}
|
|
270
284
|
onBlur={(e) => setProvider('imagegen_provider', e.target.value)} />
|
|
271
285
|
</div>
|
|
272
|
-
|
|
286
|
+
</div>
|
|
287
|
+
</details>
|
|
273
288
|
{restartPending && (
|
|
274
289
|
<div className="crew-quick-row">
|
|
275
|
-
<
|
|
290
|
+
<a className="crew-quick-btn primary" href={FULL} target="_blank" rel="noopener noreferrer">{t.openFull}</a>
|
|
276
291
|
</div>
|
|
277
292
|
)}
|
|
278
|
-
{notice && <div className="crew-quick-notice">{notice}</div>}
|
|
293
|
+
{notice && <div className="crew-quick-notice" role="status" aria-live="polite">{notice}</div>}
|
|
279
294
|
</div>
|
|
280
295
|
);
|
|
281
296
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from '../runtime-identity-contract.mjs';
|
|
2
|
+
import { validateModelCallabilityV2 } from '../model-callability-contract.mjs';
|
|
3
|
+
|
|
4
|
+
export function readinessExpiryDelay(expiresAt, now = Date.now()) {
|
|
5
|
+
return Number.isFinite(expiresAt) ? Math.max(0, expiresAt - now + 10) : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Apply one extension response to the atomic runtime/readiness envelope.
|
|
10
|
+
* Older responses are ignored so a restart cannot be rolled back by a slow
|
|
11
|
+
* request that started before the new runtime identity was observed.
|
|
12
|
+
*/
|
|
13
|
+
export function acceptReadinessResponse(response, { generation, latestGeneration } = {}) {
|
|
14
|
+
if (generation !== latestGeneration) return { accepted: false, envelope: null };
|
|
15
|
+
const extensionRuntime = response?.extension?.runtime;
|
|
16
|
+
const snapshot = response?.extension?.readiness_snapshot;
|
|
17
|
+
const valid = response?.ok === true
|
|
18
|
+
&& isExactNativeCrewIdentity(extensionRuntime)
|
|
19
|
+
&& isExactNativeCrewIdentity(snapshot?.runtime)
|
|
20
|
+
&& sameCompleteRuntimeIdentity(extensionRuntime, snapshot.runtime);
|
|
21
|
+
const expectedEnabledRoles = {
|
|
22
|
+
worker: response?.extension?.capabilities?.['deepseek.worker'] === true,
|
|
23
|
+
reviewer: response?.extension?.capabilities?.['deepseek.reviewer'] === true,
|
|
24
|
+
};
|
|
25
|
+
const projection = valid ? validateModelCallabilityV2({
|
|
26
|
+
projection: snapshot.model_callability,
|
|
27
|
+
runtime: extensionRuntime,
|
|
28
|
+
expectedEnabledRoles,
|
|
29
|
+
expectedSelections: { worker: snapshot.worker?.selected ?? null, reviewer: snapshot.reviewer?.selected ?? null },
|
|
30
|
+
}) : { ok: false };
|
|
31
|
+
return { accepted: true, envelope: valid && projection.ok
|
|
32
|
+
? { runtime: extensionRuntime, snapshot, expiresAt: snapshot.model_callability.expires_at }
|
|
33
|
+
: {} };
|
|
34
|
+
}
|
|
@@ -21,9 +21,11 @@ function hasInvocationEvidence(job) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Build a bounded, presentation-only model activity summary from
|
|
25
|
-
* Hub already exposes.
|
|
26
|
-
*
|
|
24
|
+
* Build a bounded, presentation-only, job-level model activity summary from
|
|
25
|
+
* the jobs the Hub already exposes. Each qualifying job contributes one row
|
|
26
|
+
* count for its selected provider/model; the count is not an inferred number
|
|
27
|
+
* of underlying model turns. No prompts, results, credentials, or new
|
|
28
|
+
* persistence are introduced here.
|
|
27
29
|
*/
|
|
28
30
|
export function aggregateModelInvocations(jobs = []) {
|
|
29
31
|
const groups = new Map();
|
|
@@ -45,7 +47,12 @@ export function aggregateModelInvocations(jobs = []) {
|
|
|
45
47
|
current.count += 1;
|
|
46
48
|
current.taskSources.add(clean(job?.source, 'api'));
|
|
47
49
|
current.selectionSources.add(clean(job?.selection_source, 'unknown'));
|
|
48
|
-
|
|
50
|
+
// Do not infer a role from malformed, missing, or future role values. The
|
|
51
|
+
// summary is presentation evidence and must never assert a role the job
|
|
52
|
+
// did not provide.
|
|
53
|
+
if (job?.role === 'worker' || job?.role === 'reviewer') {
|
|
54
|
+
current.roles.add(job.role);
|
|
55
|
+
}
|
|
49
56
|
const calledAt = validTimestamp(job?.startedAt);
|
|
50
57
|
if (calledAt && (!current.lastCalledAt || Date.parse(calledAt) > Date.parse(current.lastCalledAt))) {
|
|
51
58
|
current.lastCalledAt = calledAt;
|