@ran-sh/dsh-crew 1.9.0 → 1.10.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 (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +3 -2
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -340
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +463 -455
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -1,55 +1,55 @@
1
- export const SETTINGS_SECTION_STORAGE_KEY = 'dsh-crew.settings-sections.v1';
2
-
3
- export const SETTINGS_SECTION_IDS = Object.freeze([
4
- 'integrations',
5
- 'workflow',
6
- 'flash',
7
- 'pro',
8
- 'dispatch',
9
- 'adaptive',
10
- 'runtime',
11
- 'harnessProviders',
12
- 'jobs',
13
- ]);
14
-
15
- export function createDefaultSectionState() {
16
- return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, id === 'workflow']));
17
- }
18
-
19
- export function readSectionState(storage, key = SETTINGS_SECTION_STORAGE_KEY) {
20
- const defaults = createDefaultSectionState();
21
- if (!storage?.getItem) return defaults;
22
- try {
23
- const parsed = JSON.parse(storage.getItem(key) ?? 'null');
24
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return defaults;
25
- for (const id of SETTINGS_SECTION_IDS) {
26
- if (typeof parsed[id] === 'boolean') defaults[id] = parsed[id];
27
- }
28
- } catch {
29
- return defaults;
30
- }
31
- return defaults;
32
- }
33
-
34
- export function writeSectionState(storage, state, key = SETTINGS_SECTION_STORAGE_KEY) {
35
- if (!storage?.setItem) return;
36
- try {
37
- storage.setItem(key, JSON.stringify(
38
- Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, state[id] === true])),
39
- ));
40
- } catch {
41
- // Storage can be unavailable in private/locked-down browser contexts.
42
- }
43
- }
44
-
45
- export function setEverySection(expanded) {
46
- return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, expanded === true]));
47
- }
48
-
49
- export function openSections(state, ids) {
50
- const next = { ...state };
51
- for (const id of ids) {
52
- if (SETTINGS_SECTION_IDS.includes(id)) next[id] = true;
53
- }
54
- return next;
55
- }
1
+ export const SETTINGS_SECTION_STORAGE_KEY = 'dsh-crew.settings-sections.v1';
2
+
3
+ export const SETTINGS_SECTION_IDS = Object.freeze([
4
+ 'integrations',
5
+ 'workflow',
6
+ 'flash',
7
+ 'pro',
8
+ 'dispatch',
9
+ 'adaptive',
10
+ 'runtime',
11
+ 'harnessProviders',
12
+ 'jobs',
13
+ ]);
14
+
15
+ export function createDefaultSectionState() {
16
+ return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, id === 'workflow']));
17
+ }
18
+
19
+ export function readSectionState(storage, key = SETTINGS_SECTION_STORAGE_KEY) {
20
+ const defaults = createDefaultSectionState();
21
+ if (!storage?.getItem) return defaults;
22
+ try {
23
+ const parsed = JSON.parse(storage.getItem(key) ?? 'null');
24
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return defaults;
25
+ for (const id of SETTINGS_SECTION_IDS) {
26
+ if (typeof parsed[id] === 'boolean') defaults[id] = parsed[id];
27
+ }
28
+ } catch {
29
+ return defaults;
30
+ }
31
+ return defaults;
32
+ }
33
+
34
+ export function writeSectionState(storage, state, key = SETTINGS_SECTION_STORAGE_KEY) {
35
+ if (!storage?.setItem) return;
36
+ try {
37
+ storage.setItem(key, JSON.stringify(
38
+ Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, state[id] === true])),
39
+ ));
40
+ } catch {
41
+ // Storage can be unavailable in private/locked-down browser contexts.
42
+ }
43
+ }
44
+
45
+ export function setEverySection(expanded) {
46
+ return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, expanded === true]));
47
+ }
48
+
49
+ export function openSections(state, ids) {
50
+ const next = { ...state };
51
+ for (const id of ids) {
52
+ if (SETTINGS_SECTION_IDS.includes(id)) next[id] = true;
53
+ }
54
+ return next;
55
+ }
@@ -1,108 +1,108 @@
1
- import { useEffect, useState } from 'react';
2
-
3
- const API = '/_dsh/dsh-crew/history/';
4
- const pending = (phase?: string) => !!phase && !['IDLE', 'DONE', 'FAILED', 'ROLLED_BACK'].includes(phase);
5
- const copy = {
6
- zh: {
7
- title: '工作区与会话清理', intro: '仅影响 3210 的工作区记录与会话日志;不删除项目文件、附件或 3080 数据。',
8
- operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '范围', all: '全部(含你自己的会话)', crew: '仅 Crew 创建(推荐)', worktree: '仅 Crew 工作树', before: '指定时间之前',
9
- time: '创建时间早于', timeHint: '按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。',
10
- preview: '预览清理范围', confirm: '确认执行', acknowledgement: '删除确认:请输入 DELETE', idle: '会短暂停止并重启 3210;请先结束或关闭所有正在使用的 3210 会话。',
11
- consent: '我已确认范围,并同意短暂重启 3210', restore: '恢复', archives: '已归档批次', empty: '暂无可恢复的归档',
12
- working: '维护中,正在等待 3210 重新连接…', refresh: '刷新页面与工作区列表', recover: '恢复未完成的维护',
13
- recovery: '维护尚未完成,请使用恢复按钮;若 3210 无法连接,在终端运行 dsh-crew history recover。',
14
- cancelQueued: '取消尚未开始的维护', cancelQueuedHint: '仅取消尚未开始的维护,不清理任何数据;如果操作已开始,请等待完成。',
15
- count: (w: number, s: number) => `${w} 个工作区 · ${s} 个会话`, confirmRestore: '恢复这一批归档?将短暂重启 3210,不会覆盖冲突数据。',
16
- phase: { IDLE: '就绪', QUEUED: '已提交', STOPPING: '停止后台', APPLYING: '处理数据', STARTING: '启动后台', VERIFYING: '验证结果', DONE: '已完成', FAILED: '未执行成功', ROLLED_BACK: '已回滚', RECOVERY_REQUIRED: '需要恢复' },
17
- },
18
- en: {
19
- title: 'Workspace & session cleanup', intro: 'Only 3210 workspace records and session logs. Project files, attachments and 3080 data are untouched.',
20
- operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Scope', all: 'Everything (includes your own sessions)', crew: 'Crew-created only (recommended)', worktree: 'Crew worktrees only', before: 'Before a date',
21
- time: 'Created before', timeHint: 'Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.',
22
- preview: 'Preview cleanup', confirm: 'Confirm operation', acknowledgement: 'Type DELETE to confirm deletion', idle: '3210 will briefly stop and restart. End or close all active 3210 conversations first.',
23
- consent: 'I reviewed the scope and agree to restart 3210', restore: 'Restore', archives: 'Archived batches', empty: 'No restorable archives',
24
- working: 'Maintenance in progress; waiting for 3210 to reconnect…', refresh: 'Reload page and workspace list', recover: 'Recover unfinished maintenance',
25
- recovery: 'Maintenance is unfinished. Use recovery; if 3210 is unreachable, run dsh-crew history recover in a terminal.',
26
- cancelQueued: 'Cancel unstarted maintenance', cancelQueuedHint: 'Cancel only if maintenance has not started. No data is cleaned; if already running, wait for completion.',
27
- count: (w: number, s: number) => `${w} workspaces · ${s} sessions`, confirmRestore: 'Restore this archive? 3210 will briefly restart; conflicting data will not be overwritten.',
28
- phase: { IDLE: 'Ready', QUEUED: 'Queued', STOPPING: 'Stopping backend', APPLYING: 'Processing data', STARTING: 'Starting backend', VERIFYING: 'Verifying', DONE: 'Completed', FAILED: 'Failed', ROLLED_BACK: 'Rolled back', RECOVERY_REQUIRED: 'Recovery required' },
29
- },
30
- };
31
-
32
- export function HistoryPanel({ locale }: { locale: string }) {
33
- const t = copy[locale === 'zh' ? 'zh' : 'en'];
34
- const [open, setOpen] = useState(false);
35
- const [operation, setOperation] = useState('archive');
36
- const [scope, setScope] = useState('crew');
37
- const [before, setBefore] = useState('');
38
- const [preview, setPreview] = useState<any>(null);
39
- const [status, setStatus] = useState<any>({ phase: 'IDLE' });
40
- const [archives, setArchives] = useState<any[]>([]);
41
- const [busy, setBusy] = useState(false);
42
- const [consent, setConsent] = useState(false);
43
- const [ack, setAck] = useState('');
44
- const [notice, setNotice] = useState('');
45
- const [connected, setConnected] = useState(false);
46
- const locked = busy || pending(status.phase);
47
- const invalidate = () => { setPreview(null); setConsent(false); setAck(''); };
48
-
49
- async function request(action: string, body?: unknown) {
50
- const res = await fetch(API + action, { cache: 'no-store', ...(body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }) });
51
- const data = await res.json();
52
- if (!res.ok || !data.ok) throw Error(data.code ?? `HTTP ${res.status}`);
53
- return data;
54
- }
55
- useEffect(() => {
56
- if (!open) return;
57
- let disposed = false;
58
- const poll = async () => {
59
- try {
60
- const [s, a] = await Promise.all([request('status'), request('archives')]);
61
- if (!disposed) { setStatus(s.status); setArchives(a.archives); setConnected(true); }
62
- } catch { if (!disposed) setConnected(false); }
63
- };
64
- void poll(); const timer = setInterval(poll, 2500);
65
- return () => { disposed = true; clearInterval(timer); };
66
- }, [open]);
67
-
68
- async function act(action: string, body: any) {
69
- setBusy(true); setNotice('');
70
- try {
71
- const result = await request(action, body);
72
- if (result.preview) { setPreview(result.preview); setConsent(false); setAck(''); }
73
- if (result.status) { setStatus(result.status); setPreview(null); }
74
- } catch (error: any) { setNotice(String(error.message)); setPreview(null); }
75
- finally { setBusy(false); }
76
- }
77
- const style = { border: '1px solid rgba(128,128,128,.25)', borderRadius: 8, padding: '7px 10px', color: 'inherit', background: 'transparent', font: 'inherit' };
78
- return <details style={{ border: '1px solid rgba(128,128,128,.24)', borderRadius: 10, padding: '10px 13px' }} onToggle={e => setOpen(e.currentTarget.open)}>
79
- <summary style={{ cursor: 'pointer', fontWeight: 650 }}>{t.title}</summary>
80
- {open && <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 }}>
81
- <p style={{ margin: 0, opacity: .75 }}>{t.intro}</p>
82
- <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
83
- <label>{t.operation} <select style={style} value={operation} disabled={locked} onChange={e => { setOperation(e.target.value); invalidate(); }}><option value="archive">{t.archive}</option><option value="delete">{t.delete}</option></select></label>
84
- <label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="crew">{t.crew}</option><option value="worktree">{t.worktree}</option><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
85
- </div>
86
- {scope === 'before' && <label>{t.time} <input type="datetime-local" style={style} value={before} disabled={locked} onChange={e => { setBefore(e.target.value); invalidate(); }} /></label>}
87
- <div style={{ fontSize: 11.5, opacity: .7 }}>{t.timeHint}</div>
88
- <button type="button" style={style} disabled={locked || !connected || (scope === 'before' && !Number.isFinite(new Date(before).getTime()))} onClick={() => void act('preview', { operation, scope, ...(scope === 'before' ? { before: new Date(before).toISOString() } : {}) })}>{t.preview}</button>
89
- {preview && <div style={{ ...style, display: 'flex', flexDirection: 'column', gap: 9 }}>
90
- <strong>{t.count(preview.counts.workspaces, preview.counts.sessions)}</strong>
91
- {preview.before && <span>{t.time}: {new Date(preview.before).toLocaleString()}</span>}
92
- <details><summary>{t.preview}</summary><ul>{preview.items?.map((item: any) => <li key={item.id}>{item.title} <small>{item.id}</small></li>)}</ul><div style={{ overflowWrap: 'anywhere', fontSize: 11 }}>{preview.sessionIds?.slice(0, 100).join(', ')}{preview.sessionIds?.length > 100 ? ' …' : ''}</div></details>
93
- {!preview.executable && <div role="alert">{preview.blockedReason}</div>}
94
- <div>{t.idle}</div>
95
- <label><input type="checkbox" checked={consent} disabled={locked} onChange={e => setConsent(e.target.checked)} /> {t.consent}</label>
96
- {operation === 'delete' && <label>{t.acknowledgement} <input style={style} value={ack} disabled={locked} onChange={e => setAck(e.target.value)} /></label>}
97
- <button type="button" style={style} disabled={locked || !consent || !preview.executable || (operation === 'delete' && ack !== 'DELETE')} onClick={() => void act('execute', { planId: preview.planId, confirm: true, acknowledgement: ack })}>{t.confirm}</button>
98
- </div>}
99
- <div role="status" aria-live="polite">{(t.phase as any)[status.phase] ?? status.phase}{status.code ? ` · ${status.code}` : ''}{notice ? ` · ${notice}` : ''}</div>
100
- {!connected && <div role="status">{pending(status.phase) ? t.working : '3210 history API unavailable'}<p>{t.recovery}</p></div>}
101
- {['RECOVERY_REQUIRED', 'QUEUED'].includes(status.phase) && <div><p>{status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery}</p><button type="button" style={style} disabled={busy} onClick={() => { if (window.confirm(status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery)) void act('recover', { confirm: true }); }}>{status.phase === 'QUEUED' ? t.cancelQueued : t.recover}</button></div>}
102
- {['DONE', 'ROLLED_BACK'].includes(status.phase) && <button type="button" style={style} onClick={() => window.location.reload()}>{t.refresh}</button>}
103
- <strong>{t.archives}</strong>
104
- {archives.length === 0 && <span style={{ opacity: .6 }}>{t.empty}</span>}
105
- {archives.map(a => <div key={a.id} style={{ ...style, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}><span style={{ flex: 1 }}>{a.invalid ? `${a.id} · ${a.code}` : `${new Date(a.createdAt).toLocaleString()} · ${t.count(a.workspaces, a.sessions)}`}</span><button type="button" style={style} disabled={locked || a.invalid} onClick={() => { if (window.confirm(t.confirmRestore)) void act('restore', { archiveId: a.id, confirm: true }); }}>{t.restore}</button></div>)}
106
- </div>}
107
- </details>;
108
- }
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const API = '/_dsh/dsh-crew/history/';
4
+ const pending = (phase?: string) => !!phase && !['IDLE', 'DONE', 'FAILED', 'ROLLED_BACK'].includes(phase);
5
+ const copy = {
6
+ zh: {
7
+ title: '工作区与会话清理', intro: '仅影响 3210 的工作区记录与会话日志;不删除项目文件、附件或 3080 数据。',
8
+ operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '范围', all: '全部(含你自己的会话)', crew: '仅 Crew 创建(推荐)', worktree: '仅 Crew 工作树', before: '指定时间之前',
9
+ time: '创建时间早于', timeHint: '按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。',
10
+ preview: '预览清理范围', confirm: '确认执行', acknowledgement: '删除确认:请输入 DELETE', idle: '会短暂停止并重启 3210;请先结束或关闭所有正在使用的 3210 会话。',
11
+ consent: '我已确认范围,并同意短暂重启 3210', restore: '恢复', archives: '已归档批次', empty: '暂无可恢复的归档',
12
+ working: '维护中,正在等待 3210 重新连接…', refresh: '刷新页面与工作区列表', recover: '恢复未完成的维护',
13
+ recovery: '维护尚未完成,请使用恢复按钮;若 3210 无法连接,在终端运行 dsh-crew history recover。',
14
+ cancelQueued: '取消尚未开始的维护', cancelQueuedHint: '仅取消尚未开始的维护,不清理任何数据;如果操作已开始,请等待完成。',
15
+ count: (w: number, s: number) => `${w} 个工作区 · ${s} 个会话`, confirmRestore: '恢复这一批归档?将短暂重启 3210,不会覆盖冲突数据。',
16
+ phase: { IDLE: '就绪', QUEUED: '已提交', STOPPING: '停止后台', APPLYING: '处理数据', STARTING: '启动后台', VERIFYING: '验证结果', DONE: '已完成', FAILED: '未执行成功', ROLLED_BACK: '已回滚', RECOVERY_REQUIRED: '需要恢复' },
17
+ },
18
+ en: {
19
+ title: 'Workspace & session cleanup', intro: 'Only 3210 workspace records and session logs. Project files, attachments and 3080 data are untouched.',
20
+ operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Scope', all: 'Everything (includes your own sessions)', crew: 'Crew-created only (recommended)', worktree: 'Crew worktrees only', before: 'Before a date',
21
+ time: 'Created before', timeHint: 'Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.',
22
+ preview: 'Preview cleanup', confirm: 'Confirm operation', acknowledgement: 'Type DELETE to confirm deletion', idle: '3210 will briefly stop and restart. End or close all active 3210 conversations first.',
23
+ consent: 'I reviewed the scope and agree to restart 3210', restore: 'Restore', archives: 'Archived batches', empty: 'No restorable archives',
24
+ working: 'Maintenance in progress; waiting for 3210 to reconnect…', refresh: 'Reload page and workspace list', recover: 'Recover unfinished maintenance',
25
+ recovery: 'Maintenance is unfinished. Use recovery; if 3210 is unreachable, run dsh-crew history recover in a terminal.',
26
+ cancelQueued: 'Cancel unstarted maintenance', cancelQueuedHint: 'Cancel only if maintenance has not started. No data is cleaned; if already running, wait for completion.',
27
+ count: (w: number, s: number) => `${w} workspaces · ${s} sessions`, confirmRestore: 'Restore this archive? 3210 will briefly restart; conflicting data will not be overwritten.',
28
+ phase: { IDLE: 'Ready', QUEUED: 'Queued', STOPPING: 'Stopping backend', APPLYING: 'Processing data', STARTING: 'Starting backend', VERIFYING: 'Verifying', DONE: 'Completed', FAILED: 'Failed', ROLLED_BACK: 'Rolled back', RECOVERY_REQUIRED: 'Recovery required' },
29
+ },
30
+ };
31
+
32
+ export function HistoryPanel({ locale }: { locale: string }) {
33
+ const t = copy[locale === 'zh' ? 'zh' : 'en'];
34
+ const [open, setOpen] = useState(false);
35
+ const [operation, setOperation] = useState('archive');
36
+ const [scope, setScope] = useState('crew');
37
+ const [before, setBefore] = useState('');
38
+ const [preview, setPreview] = useState<any>(null);
39
+ const [status, setStatus] = useState<any>({ phase: 'IDLE' });
40
+ const [archives, setArchives] = useState<any[]>([]);
41
+ const [busy, setBusy] = useState(false);
42
+ const [consent, setConsent] = useState(false);
43
+ const [ack, setAck] = useState('');
44
+ const [notice, setNotice] = useState('');
45
+ const [connected, setConnected] = useState(false);
46
+ const locked = busy || pending(status.phase);
47
+ const invalidate = () => { setPreview(null); setConsent(false); setAck(''); };
48
+
49
+ async function request(action: string, body?: unknown) {
50
+ const res = await fetch(API + action, { cache: 'no-store', ...(body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }) });
51
+ const data = await res.json();
52
+ if (!res.ok || !data.ok) throw Error(data.code ?? `HTTP ${res.status}`);
53
+ return data;
54
+ }
55
+ useEffect(() => {
56
+ if (!open) return;
57
+ let disposed = false;
58
+ const poll = async () => {
59
+ try {
60
+ const [s, a] = await Promise.all([request('status'), request('archives')]);
61
+ if (!disposed) { setStatus(s.status); setArchives(a.archives); setConnected(true); }
62
+ } catch { if (!disposed) setConnected(false); }
63
+ };
64
+ void poll(); const timer = setInterval(poll, 2500);
65
+ return () => { disposed = true; clearInterval(timer); };
66
+ }, [open]);
67
+
68
+ async function act(action: string, body: any) {
69
+ setBusy(true); setNotice('');
70
+ try {
71
+ const result = await request(action, body);
72
+ if (result.preview) { setPreview(result.preview); setConsent(false); setAck(''); }
73
+ if (result.status) { setStatus(result.status); setPreview(null); }
74
+ } catch (error: any) { setNotice(String(error.message)); setPreview(null); }
75
+ finally { setBusy(false); }
76
+ }
77
+ const style = { border: '1px solid rgba(128,128,128,.25)', borderRadius: 8, padding: '7px 10px', color: 'inherit', background: 'transparent', font: 'inherit' };
78
+ return <details style={{ border: '1px solid rgba(128,128,128,.24)', borderRadius: 10, padding: '10px 13px' }} onToggle={e => setOpen(e.currentTarget.open)}>
79
+ <summary style={{ cursor: 'pointer', fontWeight: 650 }}>{t.title}</summary>
80
+ {open && <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 }}>
81
+ <p style={{ margin: 0, opacity: .75 }}>{t.intro}</p>
82
+ <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
83
+ <label>{t.operation} <select style={style} value={operation} disabled={locked} onChange={e => { setOperation(e.target.value); invalidate(); }}><option value="archive">{t.archive}</option><option value="delete">{t.delete}</option></select></label>
84
+ <label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="crew">{t.crew}</option><option value="worktree">{t.worktree}</option><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
85
+ </div>
86
+ {scope === 'before' && <label>{t.time} <input type="datetime-local" style={style} value={before} disabled={locked} onChange={e => { setBefore(e.target.value); invalidate(); }} /></label>}
87
+ <div style={{ fontSize: 11.5, opacity: .7 }}>{t.timeHint}</div>
88
+ <button type="button" style={style} disabled={locked || !connected || (scope === 'before' && !Number.isFinite(new Date(before).getTime()))} onClick={() => void act('preview', { operation, scope, ...(scope === 'before' ? { before: new Date(before).toISOString() } : {}) })}>{t.preview}</button>
89
+ {preview && <div style={{ ...style, display: 'flex', flexDirection: 'column', gap: 9 }}>
90
+ <strong>{t.count(preview.counts.workspaces, preview.counts.sessions)}</strong>
91
+ {preview.before && <span>{t.time}: {new Date(preview.before).toLocaleString()}</span>}
92
+ <details><summary>{t.preview}</summary><ul>{preview.items?.map((item: any) => <li key={item.id}>{item.title} <small>{item.id}</small></li>)}</ul><div style={{ overflowWrap: 'anywhere', fontSize: 11 }}>{preview.sessionIds?.slice(0, 100).join(', ')}{preview.sessionIds?.length > 100 ? ' …' : ''}</div></details>
93
+ {!preview.executable && <div role="alert">{preview.blockedReason}</div>}
94
+ <div>{t.idle}</div>
95
+ <label><input type="checkbox" checked={consent} disabled={locked} onChange={e => setConsent(e.target.checked)} /> {t.consent}</label>
96
+ {operation === 'delete' && <label>{t.acknowledgement} <input style={style} value={ack} disabled={locked} onChange={e => setAck(e.target.value)} /></label>}
97
+ <button type="button" style={style} disabled={locked || !consent || !preview.executable || (operation === 'delete' && ack !== 'DELETE')} onClick={() => void act('execute', { planId: preview.planId, confirm: true, acknowledgement: ack })}>{t.confirm}</button>
98
+ </div>}
99
+ <div role="status" aria-live="polite">{(t.phase as any)[status.phase] ?? status.phase}{status.code ? ` · ${status.code}` : ''}{notice ? ` · ${notice}` : ''}</div>
100
+ {!connected && <div role="status">{pending(status.phase) ? t.working : '3210 history API unavailable'}<p>{t.recovery}</p></div>}
101
+ {['RECOVERY_REQUIRED', 'QUEUED'].includes(status.phase) && <div><p>{status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery}</p><button type="button" style={style} disabled={busy} onClick={() => { if (window.confirm(status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery)) void act('recover', { confirm: true }); }}>{status.phase === 'QUEUED' ? t.cancelQueued : t.recover}</button></div>}
102
+ {['DONE', 'ROLLED_BACK'].includes(status.phase) && <button type="button" style={style} onClick={() => window.location.reload()}>{t.refresh}</button>}
103
+ <strong>{t.archives}</strong>
104
+ {archives.length === 0 && <span style={{ opacity: .6 }}>{t.empty}</span>}
105
+ {archives.map(a => <div key={a.id} style={{ ...style, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}><span style={{ flex: 1 }}>{a.invalid ? `${a.id} · ${a.code}` : `${new Date(a.createdAt).toLocaleString()} · ${t.count(a.workspaces, a.sessions)}`}</span><button type="button" style={style} disabled={locked || a.invalid} onClick={() => { if (window.confirm(t.confirmRestore)) void act('restore', { archiveId: a.id, confirm: true }); }}>{t.restore}</button></div>)}
106
+ </div>}
107
+ </details>;
108
+ }
@@ -1,71 +1,71 @@
1
- import { isCompleteRuntimeIdentity, isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from '../runtime-identity-contract.mjs';
2
-
3
- export const READINESS_STATES = Object.freeze({
4
- READY: 'READY',
5
- DEGRADED: 'DEGRADED',
6
- UNAVAILABLE: 'UNAVAILABLE',
7
- UNKNOWN: 'UNKNOWN',
8
- NOT_APPLICABLE: 'NOT_APPLICABLE',
9
- });
10
-
11
- const MAX_RUNTIME_SNAPSHOT_AGE_MS = 5 * 60 * 1000;
12
-
13
- function componentState(status, keys, { aligned = false } = {}) {
14
- if (status === undefined || status === null || typeof status !== 'object') return READINESS_STATES.UNKNOWN;
15
- if (status.installed === false) return READINESS_STATES.UNAVAILABLE;
16
- if (status.installed !== true || !status.components || typeof status.components !== 'object') return READINESS_STATES.UNKNOWN;
17
- const evidence = keys.map((key) => status.components[key]);
18
- if (evidence.some((value) => typeof value !== 'boolean')) return READINESS_STATES.UNKNOWN;
19
- const ready = evidence.every(Boolean) && (!aligned || status.components.target_alignment === true);
20
- return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
21
- }
22
-
23
- function runtimeState(runtime, readinessSnapshot) {
24
- if (readinessSnapshot && typeof readinessSnapshot === 'object') {
25
- const identity = readinessSnapshot.runtime;
26
- if (!isExactNativeCrewIdentity(identity)) {
27
- return READINESS_STATES.UNAVAILABLE;
28
- }
29
- if (runtime && (!isCompleteRuntimeIdentity(runtime) || !sameCompleteRuntimeIdentity(runtime, identity))) {
30
- return READINESS_STATES.UNKNOWN;
31
- }
32
- if (!Number.isFinite(readinessSnapshot.captured_at)) return READINESS_STATES.UNKNOWN;
33
- if (readinessSnapshot.captured_at > Date.now() + 30_000) return READINESS_STATES.UNKNOWN;
34
- if (Date.now() - readinessSnapshot.captured_at > MAX_RUNTIME_SNAPSHOT_AGE_MS) return READINESS_STATES.DEGRADED;
35
- const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows)
36
- ? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === 'hub_compatibility') : undefined;
37
- if (row?.status === 'PASS') return READINESS_STATES.READY;
38
- if (row?.status === 'FAIL') return READINESS_STATES.UNAVAILABLE;
39
- if (row?.status === 'NOT_RUN' || row?.status === 'SKIP') return READINESS_STATES.DEGRADED;
40
- return READINESS_STATES.UNKNOWN;
41
- }
42
- if (runtime === undefined) return READINESS_STATES.UNKNOWN;
43
- if (runtime === null) return READINESS_STATES.UNAVAILABLE;
44
- const exactIdentity = runtime?.ok === true && isExactNativeCrewIdentity(runtime);
45
- return exactIdentity ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN;
46
- }
47
-
48
- function bridgeState(surface) {
49
- // The official 3080 quick surface is an OPTIONAL convenience panel. Its
50
- // absence must never degrade Crew readiness: on the native 3210 control
51
- // plane the bridge is simply not part of the picture.
52
- if (surface === 'official-bridge') return READINESS_STATES.READY;
53
- if (surface === 'native-crew-harness') return READINESS_STATES.NOT_APPLICABLE;
54
- return READINESS_STATES.UNKNOWN;
55
- }
56
-
57
- /** Project only structured, non-secret installer/runtime evidence. */
58
- export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
59
- const codex = installStatus?.codex;
60
- const claude = installStatus?.claude;
61
- const zcode = installStatus?.zcode;
62
- return [
63
- { id: 'codex_mcp', state: componentState(codex, ['mcp'], { aligned: true }) },
64
- { id: 'ds_worker', state: componentState(codex, ['worker_role'], { aligned: true }) },
65
- { id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
66
- { id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
67
- { id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
68
- { id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
69
- { id: 'official_bridge', state: bridgeState(surface) },
70
- ];
71
- }
1
+ import { isCompleteRuntimeIdentity, isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from '../runtime-identity-contract.mjs';
2
+
3
+ export const READINESS_STATES = Object.freeze({
4
+ READY: 'READY',
5
+ DEGRADED: 'DEGRADED',
6
+ UNAVAILABLE: 'UNAVAILABLE',
7
+ UNKNOWN: 'UNKNOWN',
8
+ NOT_APPLICABLE: 'NOT_APPLICABLE',
9
+ });
10
+
11
+ const MAX_RUNTIME_SNAPSHOT_AGE_MS = 5 * 60 * 1000;
12
+
13
+ function componentState(status, keys, { aligned = false } = {}) {
14
+ if (status === undefined || status === null || typeof status !== 'object') return READINESS_STATES.UNKNOWN;
15
+ if (status.installed === false) return READINESS_STATES.UNAVAILABLE;
16
+ if (status.installed !== true || !status.components || typeof status.components !== 'object') return READINESS_STATES.UNKNOWN;
17
+ const evidence = keys.map((key) => status.components[key]);
18
+ if (evidence.some((value) => typeof value !== 'boolean')) return READINESS_STATES.UNKNOWN;
19
+ const ready = evidence.every(Boolean) && (!aligned || status.components.target_alignment === true);
20
+ return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
21
+ }
22
+
23
+ function runtimeState(runtime, readinessSnapshot) {
24
+ if (readinessSnapshot && typeof readinessSnapshot === 'object') {
25
+ const identity = readinessSnapshot.runtime;
26
+ if (!isExactNativeCrewIdentity(identity)) {
27
+ return READINESS_STATES.UNAVAILABLE;
28
+ }
29
+ if (runtime && (!isCompleteRuntimeIdentity(runtime) || !sameCompleteRuntimeIdentity(runtime, identity))) {
30
+ return READINESS_STATES.UNKNOWN;
31
+ }
32
+ if (!Number.isFinite(readinessSnapshot.captured_at)) return READINESS_STATES.UNKNOWN;
33
+ if (readinessSnapshot.captured_at > Date.now() + 30_000) return READINESS_STATES.UNKNOWN;
34
+ if (Date.now() - readinessSnapshot.captured_at > MAX_RUNTIME_SNAPSHOT_AGE_MS) return READINESS_STATES.DEGRADED;
35
+ const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows)
36
+ ? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === 'hub_compatibility') : undefined;
37
+ if (row?.status === 'PASS') return READINESS_STATES.READY;
38
+ if (row?.status === 'FAIL') return READINESS_STATES.UNAVAILABLE;
39
+ if (row?.status === 'NOT_RUN' || row?.status === 'SKIP') return READINESS_STATES.DEGRADED;
40
+ return READINESS_STATES.UNKNOWN;
41
+ }
42
+ if (runtime === undefined) return READINESS_STATES.UNKNOWN;
43
+ if (runtime === null) return READINESS_STATES.UNAVAILABLE;
44
+ const exactIdentity = runtime?.ok === true && isExactNativeCrewIdentity(runtime);
45
+ return exactIdentity ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN;
46
+ }
47
+
48
+ function bridgeState(surface) {
49
+ // The official 3080 quick surface is an OPTIONAL convenience panel. Its
50
+ // absence must never degrade Crew readiness: on the native 3210 control
51
+ // plane the bridge is simply not part of the picture.
52
+ if (surface === 'official-bridge') return READINESS_STATES.READY;
53
+ if (surface === 'native-crew-harness') return READINESS_STATES.NOT_APPLICABLE;
54
+ return READINESS_STATES.UNKNOWN;
55
+ }
56
+
57
+ /** Project only structured, non-secret installer/runtime evidence. */
58
+ export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
59
+ const codex = installStatus?.codex;
60
+ const claude = installStatus?.claude;
61
+ const zcode = installStatus?.zcode;
62
+ return [
63
+ { id: 'codex_mcp', state: componentState(codex, ['mcp'], { aligned: true }) },
64
+ { id: 'ds_worker', state: componentState(codex, ['worker_role'], { aligned: true }) },
65
+ { id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
66
+ { id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
67
+ { id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
68
+ { id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
69
+ { id: 'official_bridge', state: bridgeState(surface) },
70
+ ];
71
+ }