@dsh-workbench/shared 0.3.4 → 0.3.6
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/dist/api/blocker-table.d.ts +13 -0
- package/dist/api/blocker-table.js +13 -0
- package/dist/api/blocker.d.ts +6 -0
- package/dist/api/context-ref.d.ts +31 -0
- package/dist/api/context-ref.js +28 -0
- package/dist/api/dsh-client.d.ts +99 -0
- package/dist/api/dsh.d.ts +55 -3
- package/dist/api/facts.d.ts +42 -0
- package/dist/api/facts.js +33 -0
- package/dist/api/host-write.d.ts +18 -0
- package/dist/api/host-write.js +12 -0
- package/dist/api/long-task.d.ts +66 -1
- package/dist/api/long-task.js +59 -0
- package/dist/api/unit-board.d.ts +25 -0
- package/dist/api/unit-board.js +16 -0
- package/dist/api/unit-declaration.d.ts +36 -0
- package/dist/api/unit-declaration.js +24 -0
- package/dist/api/unit.d.ts +32 -0
- package/dist/api/unit.js +5 -0
- package/dist/api/workbench-client.d.ts +25 -0
- package/dist/api/workbench-client.js +13 -0
- package/dist/api/workbench-contract.d.ts +12 -0
- package/dist/components/Activity.d.ts +33 -0
- package/dist/components/Activity.js +24 -0
- package/dist/components/ApprovalDetail.d.ts +12 -0
- package/dist/components/ApprovalDetail.js +9 -0
- package/dist/components/ApprovalPlan.d.ts +14 -0
- package/dist/components/ApprovalPlan.js +8 -0
- package/dist/components/Card.d.ts +13 -0
- package/dist/components/Card.js +10 -0
- package/dist/components/CardAnchor.d.ts +14 -0
- package/dist/components/CardAnchor.js +11 -0
- package/dist/components/CardBoundary.d.ts +20 -0
- package/dist/components/CardBoundary.js +14 -0
- package/dist/components/CardHead.d.ts +13 -0
- package/dist/components/CardHead.js +10 -0
- package/dist/components/ChoiceGroup.d.ts +24 -0
- package/dist/components/ChoiceGroup.js +12 -0
- package/dist/components/Field.d.ts +10 -0
- package/dist/components/Field.js +7 -0
- package/dist/components/Form.css +6 -0
- package/dist/components/Form.d.ts +20 -0
- package/dist/components/Form.js +58 -0
- package/dist/components/FormActions.d.ts +14 -0
- package/dist/components/FormActions.js +5 -0
- package/dist/components/FormNodes.d.ts +103 -0
- package/dist/components/FormNodes.js +153 -0
- package/dist/components/FormRows.css +31 -0
- package/dist/components/FormRows.d.ts +20 -0
- package/dist/components/FormRows.js +14 -0
- package/dist/components/FormTypes.d.ts +59 -0
- package/dist/components/FormTypes.js +1 -0
- package/dist/components/Link.d.ts +12 -0
- package/dist/components/Link.js +9 -0
- package/dist/components/LongTaskRow.css +52 -0
- package/dist/components/LongTaskRow.d.ts +35 -0
- package/dist/components/LongTaskRow.js +45 -4
- package/dist/components/Note.d.ts +11 -0
- package/dist/components/Note.js +8 -0
- package/dist/components/ResourceStates.d.ts +25 -0
- package/dist/components/ResourceStates.js +22 -0
- package/dist/components/Rows.d.ts +17 -0
- package/dist/components/Rows.js +11 -0
- package/dist/components/SaveBar.d.ts +12 -0
- package/dist/components/SaveBar.js +9 -0
- package/dist/components/SelectField.d.ts +26 -0
- package/dist/components/SelectField.js +22 -0
- package/dist/components/SelectFieldNodes.d.ts +24 -0
- package/dist/components/SelectFieldNodes.js +24 -0
- package/dist/components/SettingsSection.d.ts +11 -0
- package/dist/components/SettingsSection.js +8 -0
- package/dist/components/Spinner.d.ts +9 -0
- package/dist/components/Spinner.js +6 -0
- package/dist/components/Stack.d.ts +8 -0
- package/dist/components/Stack.js +5 -0
- package/dist/components/StateBlock.d.ts +13 -0
- package/dist/components/StateBlock.js +10 -0
- package/dist/components/TextField.d.ts +8 -0
- package/dist/components/TextField.js +5 -0
- package/dist/components/board-module.d.ts +30 -0
- package/dist/components/icons.d.ts +38 -0
- package/dist/components/icons.js +35 -0
- package/dist/config/constants.d.ts +42 -0
- package/dist/config/constants.js +42 -0
- package/dist/config/storage.d.ts +4 -0
- package/dist/config/storage.js +4 -0
- package/dist/host/long-task.d.ts +7 -0
- package/dist/host/long-task.js +31 -5
- package/dist/host/tool-approval.d.ts +1 -0
- package/dist/host/unit-face.d.ts +6 -1
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.js +10 -8
- package/dist/i18n/index.d.ts +13 -9
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/merge.d.ts +5 -0
- package/dist/i18n/merge.js +2 -0
- package/dist/i18n/zh.d.ts +11 -8
- package/dist/i18n/zh.js +10 -8
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/lib/blocker-action.d.ts +22 -0
- package/dist/lib/blocker-action.js +13 -0
- package/dist/lib/board-fact.d.ts +27 -0
- package/dist/lib/board-fact.js +18 -0
- package/dist/lib/busy.d.ts +10 -0
- package/dist/lib/busy.js +7 -0
- package/dist/lib/diy-debug.d.ts +42 -2
- package/dist/lib/diy-debug.js +19 -1
- package/dist/lib/diy-record.d.ts +41 -0
- package/dist/lib/diy-record.js +17 -0
- package/dist/lib/form-problem.d.ts +10 -0
- package/dist/lib/form-problem.js +10 -0
- package/dist/lib/panel-state.d.ts +60 -0
- package/dist/lib/panel-state.js +30 -0
- package/dist/lib/react.d.ts +39 -0
- package/dist/lib/react.js +36 -0
- package/dist/lib/text.d.ts +22 -0
- package/dist/lib/text.js +22 -0
- package/dist/lib/ui.d.ts +50 -0
- package/dist/lib/ui.js +41 -0
- package/package.json +1 -1
- package/src/api/__tests__/long-task.test.ts +41 -1
- package/src/api/blocker-table.ts +13 -0
- package/src/api/blocker.ts +6 -0
- package/src/api/context-ref.ts +31 -0
- package/src/api/dsh-client.ts +99 -0
- package/src/api/dsh.ts +56 -1
- package/src/api/facts.ts +42 -0
- package/src/api/host-write.ts +18 -0
- package/src/api/long-task.ts +79 -1
- package/src/api/unit-board.ts +25 -0
- package/src/api/unit-declaration.ts +36 -0
- package/src/api/unit.ts +32 -0
- package/src/api/workbench-client.ts +25 -0
- package/src/api/workbench-contract.ts +12 -0
- package/src/components/Activity.tsx +33 -0
- package/src/components/ApprovalDetail.tsx +12 -0
- package/src/components/ApprovalPlan.tsx +14 -0
- package/src/components/Card.tsx +13 -0
- package/src/components/CardAnchor.tsx +14 -0
- package/src/components/CardBoundary.tsx +20 -0
- package/src/components/CardHead.tsx +13 -0
- package/src/components/ChoiceGroup.tsx +24 -0
- package/src/components/Field.tsx +10 -0
- package/src/components/Form.css +6 -0
- package/src/components/Form.test.tsx +475 -0
- package/src/components/Form.tsx +78 -0
- package/src/components/FormActions.tsx +14 -0
- package/src/components/FormNodes.tsx +193 -0
- package/src/components/FormRows.css +31 -0
- package/src/components/FormRows.test.tsx +71 -1
- package/src/components/FormRows.tsx +20 -0
- package/src/components/FormTypes.ts +65 -0
- package/src/components/FormWiring.test.tsx +88 -0
- package/src/components/Link.tsx +12 -0
- package/src/components/LongTaskRow.css +52 -0
- package/src/components/LongTaskRow.tsx +76 -2
- package/src/components/Note.tsx +11 -0
- package/src/components/ResourceStates.tsx +25 -0
- package/src/components/Rows.tsx +17 -0
- package/src/components/SaveBar.tsx +12 -0
- package/src/components/SelectField.tsx +26 -0
- package/src/components/SelectFieldNodes.tsx +24 -0
- package/src/components/SettingsSection.tsx +11 -0
- package/src/components/Spinner.tsx +9 -0
- package/src/components/Stack.tsx +8 -0
- package/src/components/StateBlock.tsx +13 -0
- package/src/components/TextField.tsx +8 -0
- package/src/components/__tests__/long-task-row.test.ts +115 -0
- package/src/components/board-module.ts +30 -0
- package/src/components/icons.tsx +38 -0
- package/src/config/constants.ts +43 -0
- package/src/config/storage.ts +4 -0
- package/src/host/__tests__/long-task.test.ts +264 -0
- package/src/host/long-task.ts +43 -7
- package/src/host/tool-approval.ts +1 -0
- package/src/host/unit-face.ts +7 -1
- package/src/i18n/en.ts +11 -8
- package/src/i18n/index.ts +3 -0
- package/src/i18n/merge.ts +5 -0
- package/src/i18n/zh.ts +11 -8
- package/src/index.ts +6 -2
- package/src/lib/blocker-action.ts +22 -0
- package/src/lib/board-fact.ts +27 -0
- package/src/lib/busy.ts +10 -0
- package/src/lib/diy-debug.test.ts +2 -1
- package/src/lib/diy-debug.ts +44 -3
- package/src/lib/diy-record.ts +41 -0
- package/src/lib/form-problem.ts +10 -0
- package/src/lib/panel-state.ts +60 -0
- package/src/lib/react.ts +39 -0
- package/src/lib/text.ts +22 -0
- package/src/lib/ui.ts +50 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { LongTaskStep } from '../../api/long-task.js';
|
|
3
|
+
import {
|
|
4
|
+
createLongTaskHost,
|
|
5
|
+
createLongTaskRegistry,
|
|
6
|
+
sharedLongTaskRegistry,
|
|
7
|
+
type LongTaskBase,
|
|
8
|
+
type LongTaskDeps,
|
|
9
|
+
} from '../long-task.js';
|
|
10
|
+
|
|
11
|
+
interface Probe extends LongTaskBase {
|
|
12
|
+
readonly note?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function harness(over: Partial<LongTaskDeps<Probe>> = {}) {
|
|
16
|
+
let signal: AbortSignal | undefined;
|
|
17
|
+
let step: ((step: LongTaskStep) => void) | undefined;
|
|
18
|
+
let settle: ((result: unknown) => void) | undefined;
|
|
19
|
+
let fail: ((error: unknown) => void) | undefined;
|
|
20
|
+
const base: LongTaskDeps<Probe> = {
|
|
21
|
+
now: () => '2026-10-21T02:00:00.000Z',
|
|
22
|
+
run: (options) =>
|
|
23
|
+
new Promise((resolve, reject) => {
|
|
24
|
+
signal = options.signal;
|
|
25
|
+
step = options.onStep;
|
|
26
|
+
settle = resolve as (result: unknown) => void;
|
|
27
|
+
fail = reject as (error: unknown) => void;
|
|
28
|
+
}),
|
|
29
|
+
view: (snapshot) => ({ target: snapshot.target, status: snapshot.status, note: snapshot.note }),
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
deps: { ...base, ...over },
|
|
33
|
+
signal: () => signal,
|
|
34
|
+
step: (one: LongTaskStep) => step?.(one),
|
|
35
|
+
done: (result: unknown) => settle?.(result),
|
|
36
|
+
boom: (error: unknown) => fail?.(error),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const flush = async (): Promise<void> => {
|
|
41
|
+
await Promise.resolve();
|
|
42
|
+
await Promise.resolve();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('createLongTaskRegistry.stop(停止只是转发给生产方,认不认 signal 是模块的事)', () => {
|
|
46
|
+
it('★★ stop:run 拿到的 signal 被 abort,这一趟变 stopped,回 true', () => {
|
|
47
|
+
const h = harness();
|
|
48
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
49
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
50
|
+
|
|
51
|
+
expect(h.signal()?.aborted).toBe(false);
|
|
52
|
+
expect(runs.stop(started.id)).toBe(true);
|
|
53
|
+
expect(h.signal()?.aborted).toBe(true);
|
|
54
|
+
expect(runs.snapshot(started.id)?.status).toBe('stopped');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('★★ 结算首胜:停过之后 run 再 resolve 也不许翻案(还是 stopped,不是 ok)', async () => {
|
|
58
|
+
const h = harness();
|
|
59
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
60
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
61
|
+
|
|
62
|
+
runs.stop(started.id);
|
|
63
|
+
h.done({ anything: true });
|
|
64
|
+
await flush();
|
|
65
|
+
|
|
66
|
+
expect(runs.snapshot(started.id)?.status).toBe('stopped');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('★★ 被 abort 的 run 常常 reject(AbortError):那一趟不许变成 failed', async () => {
|
|
70
|
+
const h = harness();
|
|
71
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
72
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
73
|
+
|
|
74
|
+
runs.stop(started.id);
|
|
75
|
+
h.boom(new Error('This operation was aborted'));
|
|
76
|
+
await flush();
|
|
77
|
+
|
|
78
|
+
expect(runs.snapshot(started.id)?.status).toBe('stopped');
|
|
79
|
+
expect(runs.snapshot(started.id)?.why).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('★★ 不是活的:已收尾 / 不认识的 id ⇒ 回 false,什么都没变', async () => {
|
|
83
|
+
const h = harness();
|
|
84
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
85
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
86
|
+
|
|
87
|
+
h.done({});
|
|
88
|
+
await flush();
|
|
89
|
+
|
|
90
|
+
expect(runs.stop(started.id)).toBe(false);
|
|
91
|
+
expect(runs.snapshot(started.id)?.status).toBe('ok');
|
|
92
|
+
expect(runs.stop('run-nope')).toBe(false);
|
|
93
|
+
expect(runs.current('/w/a')).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('★★ 停这一下走 deps.stopped(与 outcome 之于 ok 同形:停在哪、文案怎么写是模块的知识)', () => {
|
|
97
|
+
const h = harness({ stopped: () => ({ note: '用户按停的' }) });
|
|
98
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
99
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
100
|
+
|
|
101
|
+
runs.stop(started.id);
|
|
102
|
+
|
|
103
|
+
expect(runs.snapshot(started.id)?.note).toBe('用户按停的');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('★ 等你登录时也能停:登录地址跟着清掉', () => {
|
|
107
|
+
const h = harness();
|
|
108
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
109
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
110
|
+
|
|
111
|
+
h.step({ kind: 'start', text: '未登录', loginUrl: 'https://x/login' });
|
|
112
|
+
expect(runs.snapshot(started.id)?.status).toBe('needs-login');
|
|
113
|
+
|
|
114
|
+
expect(runs.stop(started.id)).toBe(true);
|
|
115
|
+
expect(runs.snapshot(started.id)?.status).toBe('stopped');
|
|
116
|
+
expect(runs.snapshot(started.id)?.loginUrl).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('★ 停过之后迟到的步骤也不进这一趟(结算首胜)', () => {
|
|
120
|
+
const h = harness();
|
|
121
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
122
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
123
|
+
|
|
124
|
+
runs.stop(started.id);
|
|
125
|
+
h.step({ kind: 'info', text: '迟到一步' });
|
|
126
|
+
|
|
127
|
+
expect(runs.snapshot(started.id)?.steps).toEqual([]);
|
|
128
|
+
expect(runs.snapshot(started.id)?.status).toBe('stopped');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('★★ current() 每条都带 id(浏览器按 id 点那颗钮),view 覆盖不了', () => {
|
|
132
|
+
const h = harness({ view: (snapshot) => ({ id: 'spoofed', status: snapshot.status }) });
|
|
133
|
+
const runs = createLongTaskRegistry({ ...h.deps, nextId: () => 'run-1' });
|
|
134
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
135
|
+
|
|
136
|
+
const [row] = runs.current('/w/a');
|
|
137
|
+
|
|
138
|
+
expect(started.id).toBe('run-1');
|
|
139
|
+
expect(row?.['id']).toBe('run-1');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('sharedLongTaskRegistry(那一份跨热重载存活)', () => {
|
|
144
|
+
it('★★ 槽里那份**上一版的注册表**(四个方法、没有 stop)仍被认回:升级那次重载不重建空表', () => {
|
|
145
|
+
const scope = `test-${String(Date.now())}-${Math.random()}`;
|
|
146
|
+
const slot = Symbol.for(`dsh-workbench:long-task:${scope}:demo`);
|
|
147
|
+
const stale = {
|
|
148
|
+
start: () => undefined,
|
|
149
|
+
snapshot: () => null,
|
|
150
|
+
active: () => null,
|
|
151
|
+
current: () => [],
|
|
152
|
+
onChange: () => () => {},
|
|
153
|
+
};
|
|
154
|
+
(globalThis as unknown as Record<symbol, unknown>)[slot] = stale;
|
|
155
|
+
|
|
156
|
+
const again = sharedLongTaskRegistry(scope, 'demo', harness().deps);
|
|
157
|
+
|
|
158
|
+
expect(again).toBe(stale);
|
|
159
|
+
expect(Object.hasOwn(again, 'stop')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('★ 认不出(热重载换了接口)⇒ 重造,槽里换成新表', () => {
|
|
163
|
+
const scope = `test-${String(Date.now())}-${Math.random()}`;
|
|
164
|
+
const slot = Symbol.for(`dsh-workbench:long-task:${scope}:demo`);
|
|
165
|
+
const stale = {
|
|
166
|
+
start: () => undefined,
|
|
167
|
+
snapshot: () => null,
|
|
168
|
+
active: () => null,
|
|
169
|
+
};
|
|
170
|
+
(globalThis as unknown as Record<symbol, unknown>)[slot] = stale;
|
|
171
|
+
|
|
172
|
+
const fresh = sharedLongTaskRegistry(scope, 'demo', harness().deps);
|
|
173
|
+
|
|
174
|
+
expect(fresh).not.toBe(stale);
|
|
175
|
+
expect((globalThis as unknown as Record<symbol, unknown>)[slot]).toBe(fresh);
|
|
176
|
+
expect(typeof fresh.stop).toBe('function');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('来源会话(谁的这一趟,决定停它要不要审批)', () => {
|
|
181
|
+
it('★★ start 把来源会话记进快照', () => {
|
|
182
|
+
const h = harness();
|
|
183
|
+
const runs = createLongTaskRegistry({ ...h.deps, nextId: () => 'run-1' });
|
|
184
|
+
const started = runs.start({ workspace: '/w/a', target: 'release', session: 's-1' });
|
|
185
|
+
|
|
186
|
+
expect(started.session).toBe('s-1');
|
|
187
|
+
expect(runs.snapshot('run-1')?.session).toBe('s-1');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('★★ 不给来源 ⇒ 快照里连 session 这个键都没有(别把 undefined 塞进去)', () => {
|
|
191
|
+
const h = harness();
|
|
192
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
193
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
194
|
+
|
|
195
|
+
expect(started.session).toBeUndefined();
|
|
196
|
+
expect(Object.keys(runs.snapshot(started.id) ?? {})).not.toContain('session');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('★★ stop(id, by):谁按的落在那条 settle 通知上', () => {
|
|
200
|
+
const h = harness();
|
|
201
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
202
|
+
const seen: Array<{ kind: string; by: string | undefined; status: string }> = [];
|
|
203
|
+
runs.onChange((change) => {
|
|
204
|
+
seen.push({ kind: change.kind, by: change.by, status: change.snapshot.status });
|
|
205
|
+
});
|
|
206
|
+
const started = runs.start({ workspace: '/w/a', target: 'release', session: 's-1' });
|
|
207
|
+
|
|
208
|
+
expect(runs.stop(started.id, 's-2')).toBe(true);
|
|
209
|
+
|
|
210
|
+
expect(seen).toEqual([
|
|
211
|
+
{ kind: 'start', by: undefined, status: 'running' },
|
|
212
|
+
{ kind: 'settle', by: 's-2', status: 'stopped' },
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('★★ stop(id) 不给 by ⇒ 通知里的 by 是 undefined(界面上按的用户)', () => {
|
|
217
|
+
const h = harness();
|
|
218
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
219
|
+
let settleBy: string | undefined = 'sentinel';
|
|
220
|
+
runs.onChange((change) => {
|
|
221
|
+
if (change.kind === 'settle') settleBy = change.by;
|
|
222
|
+
});
|
|
223
|
+
const started = runs.start({ workspace: '/w/a', target: 'release' });
|
|
224
|
+
|
|
225
|
+
expect(runs.stop(started.id)).toBe(true);
|
|
226
|
+
expect(settleBy).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('createLongTaskHost 的 notify(每次变更都叫一下,bump 那半照旧)', () => {
|
|
231
|
+
it('★★ 起 / 走一步 / 被停 三次都叫 notify;停那笔带着谁按的', () => {
|
|
232
|
+
const bumps: string[][] = [];
|
|
233
|
+
const seen: string[] = [];
|
|
234
|
+
const box = createLongTaskHost({
|
|
235
|
+
scope: `test-${String(Date.now())}-${Math.random()}`,
|
|
236
|
+
unit: 'demo',
|
|
237
|
+
bump: (facts) => bumps.push([...facts]),
|
|
238
|
+
notify: (change) => seen.push(`${change.kind}:${change.snapshot.status}:${change.by ?? '-'}`),
|
|
239
|
+
});
|
|
240
|
+
const h = harness();
|
|
241
|
+
const runs = box.host.create({ key: 'release', ...h.deps, nextId: () => 'run-1' });
|
|
242
|
+
|
|
243
|
+
runs.start({ workspace: '/w/a', target: 'release', session: 's-1' });
|
|
244
|
+
h.step({ kind: 'start', text: '走一步' });
|
|
245
|
+
|
|
246
|
+
expect(runs.stop('run-1', 's-1')).toBe(true);
|
|
247
|
+
expect(seen).toEqual(['start:running:-', 'step:running:-', 'settle:stopped:s-1']);
|
|
248
|
+
expect(bumps).toEqual([['demo/runs'], ['demo/runs'], ['demo/runs']]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('★ 没给 notify 也照常跑(老调用点不受影响)', () => {
|
|
252
|
+
const bumps: string[][] = [];
|
|
253
|
+
const box = createLongTaskHost({
|
|
254
|
+
scope: `test-${String(Date.now())}-${Math.random()}`,
|
|
255
|
+
unit: 'demo',
|
|
256
|
+
bump: (facts) => bumps.push([...facts]),
|
|
257
|
+
});
|
|
258
|
+
const h = harness();
|
|
259
|
+
|
|
260
|
+
box.host.create({ key: 'release', ...h.deps }).start({ workspace: '/w/a', target: 'release' });
|
|
261
|
+
|
|
262
|
+
expect(bumps).toEqual([['demo/runs']]);
|
|
263
|
+
});
|
|
264
|
+
});
|
package/src/host/long-task.ts
CHANGED
|
@@ -15,27 +15,32 @@ export interface LongTaskBase {
|
|
|
15
15
|
readonly why?: string | undefined;
|
|
16
16
|
readonly startedAt: string;
|
|
17
17
|
readonly at: string;
|
|
18
|
+
readonly session?: string | undefined;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface LongTaskStart {
|
|
21
22
|
readonly workspace: string;
|
|
22
23
|
readonly target: string;
|
|
23
24
|
readonly payload?: unknown;
|
|
25
|
+
readonly session?: string | undefined;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface LongTaskChange<S extends LongTaskBase> {
|
|
27
29
|
readonly kind: 'start' | 'step' | 'settle';
|
|
28
30
|
readonly snapshot: S;
|
|
29
31
|
readonly step?: LongTaskStep | undefined;
|
|
32
|
+
readonly by?: string | undefined;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export interface LongTaskDeps<S extends LongTaskBase> {
|
|
33
36
|
readonly run: (options: {
|
|
34
37
|
readonly payload: unknown;
|
|
35
38
|
readonly onStep: (step: LongTaskStep) => void;
|
|
39
|
+
readonly signal: AbortSignal;
|
|
36
40
|
}) => Promise<unknown>;
|
|
37
41
|
readonly step?: ((step: LongTaskStep, current: S) => Partial<S>) | undefined;
|
|
38
42
|
readonly outcome?: ((result: unknown, current: S) => Partial<S>) | undefined;
|
|
43
|
+
readonly stopped?: ((current: S) => Partial<S>) | undefined;
|
|
39
44
|
readonly view: (snapshot: S) => Readonly<Record<string, unknown>>;
|
|
40
45
|
readonly now?: (() => string) | undefined;
|
|
41
46
|
readonly nextId?: (() => string) | undefined;
|
|
@@ -48,17 +53,25 @@ export interface LongTaskRegistry<S extends LongTaskBase> {
|
|
|
48
53
|
snapshot(id: string): S | null;
|
|
49
54
|
active(workspace: string, target: string): S | null;
|
|
50
55
|
current(workspace: string): readonly Readonly<Record<string, unknown>>[];
|
|
56
|
+
stop(id: string, by?: string): boolean;
|
|
51
57
|
onChange(listener: (change: LongTaskChange<S>) => void): () => void;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDeps<S>): LongTaskRegistry<S> {
|
|
55
61
|
const runs = new Map<string, LongTaskBase>();
|
|
62
|
+
const controls = new Map<string, AbortController>();
|
|
56
63
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
57
64
|
let counter = 0;
|
|
58
65
|
const nextId = deps.nextId ?? (() => `run-${Date.now()}-${(counter += 1)}`);
|
|
59
66
|
const recentMs = deps.recentMs ?? RECENT_MS;
|
|
60
67
|
const listeners = new Set<(change: LongTaskChange<S>) => void>();
|
|
61
68
|
|
|
69
|
+
const live = (id: string): S | null => {
|
|
70
|
+
const current = runs.get(id);
|
|
71
|
+
if (current === undefined || !LIVE.has(current.status)) return null;
|
|
72
|
+
return current as S;
|
|
73
|
+
};
|
|
74
|
+
|
|
62
75
|
const notify = (change: LongTaskChange<S>): void => {
|
|
63
76
|
for (const listener of [...listeners]) {
|
|
64
77
|
try {
|
|
@@ -97,7 +110,7 @@ export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDep
|
|
|
97
110
|
(snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor,
|
|
98
111
|
)
|
|
99
112
|
.reverse()
|
|
100
|
-
.map((snapshot) => deps.view(snapshot as S));
|
|
113
|
+
.map((snapshot) => ({ ...deps.view(snapshot as S), id: snapshot.id }));
|
|
101
114
|
},
|
|
102
115
|
|
|
103
116
|
start(options) {
|
|
@@ -114,14 +127,15 @@ export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDep
|
|
|
114
127
|
steps: [],
|
|
115
128
|
startedAt,
|
|
116
129
|
at: startedAt,
|
|
130
|
+
...(options.session === undefined ? {} : { session: options.session }),
|
|
117
131
|
});
|
|
118
132
|
notify({ kind: 'start', snapshot: runs.get(id) as S });
|
|
119
133
|
|
|
120
134
|
const onStep = (step: LongTaskStep): void => {
|
|
121
|
-
const current =
|
|
122
|
-
if (current ===
|
|
135
|
+
const current = live(id);
|
|
136
|
+
if (current === null) return;
|
|
123
137
|
const steps = [...current.steps, `${step.kind === 'fail' ? '✗ ' : ''}${step.text}`].slice(-20);
|
|
124
|
-
const extra = deps.step?.(step, current
|
|
138
|
+
const extra = deps.step?.(step, current) ?? {};
|
|
125
139
|
if (step.loginUrl !== undefined) {
|
|
126
140
|
patch(
|
|
127
141
|
id,
|
|
@@ -143,17 +157,24 @@ export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDep
|
|
|
143
157
|
);
|
|
144
158
|
};
|
|
145
159
|
|
|
160
|
+
const control = new AbortController();
|
|
161
|
+
controls.set(id, control);
|
|
162
|
+
|
|
146
163
|
void deps
|
|
147
|
-
.run({ payload: options.payload, onStep })
|
|
164
|
+
.run({ payload: options.payload, onStep, signal: control.signal })
|
|
148
165
|
.then((result) => {
|
|
149
|
-
const current =
|
|
166
|
+
const current = live(id);
|
|
167
|
+
if (current === null) return;
|
|
168
|
+
controls.delete(id);
|
|
150
169
|
patch(
|
|
151
170
|
id,
|
|
152
|
-
{ status: 'ok', ...(deps.outcome?.(result, current
|
|
171
|
+
{ status: 'ok', ...(deps.outcome?.(result, current) ?? {}), loginUrl: undefined },
|
|
153
172
|
{ kind: 'settle' },
|
|
154
173
|
);
|
|
155
174
|
})
|
|
156
175
|
.catch((error: unknown) => {
|
|
176
|
+
if (live(id) === null) return;
|
|
177
|
+
controls.delete(id);
|
|
157
178
|
patch(
|
|
158
179
|
id,
|
|
159
180
|
{
|
|
@@ -168,6 +189,19 @@ export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDep
|
|
|
168
189
|
return runs.get(id) as S;
|
|
169
190
|
},
|
|
170
191
|
|
|
192
|
+
stop(id, by) {
|
|
193
|
+
const current = live(id);
|
|
194
|
+
if (current === null) return false;
|
|
195
|
+
controls.get(id)?.abort();
|
|
196
|
+
controls.delete(id);
|
|
197
|
+
patch(
|
|
198
|
+
id,
|
|
199
|
+
{ ...(deps.stopped?.(current) ?? {}), status: 'stopped', loginUrl: undefined },
|
|
200
|
+
{ kind: 'settle', by },
|
|
201
|
+
);
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
|
|
171
205
|
snapshot(id) {
|
|
172
206
|
return (runs.get(id) as S | undefined) ?? null;
|
|
173
207
|
},
|
|
@@ -212,6 +246,7 @@ export function createLongTaskHost(options: {
|
|
|
212
246
|
readonly scope: string;
|
|
213
247
|
readonly unit: string;
|
|
214
248
|
readonly bump: (facts: readonly string[]) => void;
|
|
249
|
+
readonly notify?: ((change: LongTaskChange<LongTaskBase>) => void) | undefined;
|
|
215
250
|
}): { readonly host: LongTaskHost; dispose(): void } {
|
|
216
251
|
const offs: Array<() => void> = [];
|
|
217
252
|
return {
|
|
@@ -223,6 +258,7 @@ export function createLongTaskHost(options: {
|
|
|
223
258
|
registry.onChange((change) => {
|
|
224
259
|
const named = [runsRouteOf(options.unit), ...(facts?.(change) ?? [])];
|
|
225
260
|
options.bump([...new Set(named)]);
|
|
261
|
+
options.notify?.(change);
|
|
226
262
|
}),
|
|
227
263
|
);
|
|
228
264
|
return registry;
|
package/src/host/unit-face.ts
CHANGED
|
@@ -8,6 +8,11 @@ import type { ApprovalCall, ApprovalFacts, ApprovalPolicy } from '../host/tool-a
|
|
|
8
8
|
import type { ValueHandoff } from '../host/value-handoff.js';
|
|
9
9
|
import type { WorkspaceStore } from '../host/workspace-store.js';
|
|
10
10
|
|
|
11
|
+
export interface ToolScopeContext {
|
|
12
|
+
readonly workspace: string | null;
|
|
13
|
+
readonly preset: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export interface UnitMountDeps {
|
|
12
17
|
readonly manifest: UnitManifest;
|
|
13
18
|
readonly ctx: DshHostContext;
|
|
@@ -23,6 +28,7 @@ export interface UnitMountDeps {
|
|
|
23
28
|
options?: { readonly force?: boolean },
|
|
24
29
|
) => Promise<UnitCapability | undefined>;
|
|
25
30
|
readonly longTasks?: LongTaskHost | undefined;
|
|
31
|
+
readonly longTaskRuns?: (() => ReadonlyMap<string, LongTaskRegistry<LongTaskBase>>) | undefined;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export type ContextRenderer = (ref: {
|
|
@@ -43,7 +49,7 @@ export interface UnitMountResult {
|
|
|
43
49
|
registerTools(): Array<() => void>;
|
|
44
50
|
readonly factsOf?: ((call: UnitCall) => readonly string[] | undefined) | undefined;
|
|
45
51
|
readonly gate?: {
|
|
46
|
-
deniedTools(
|
|
52
|
+
deniedTools(context: ToolScopeContext): readonly string[];
|
|
47
53
|
subscribe(listener: () => void): () => void;
|
|
48
54
|
};
|
|
49
55
|
readonly context?: {
|
package/src/i18n/en.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WorkbenchMessages } from './zh.js';
|
|
2
2
|
|
|
3
|
+
/** 外壳自带的英文词表:键必须与 `zh` 逐一对应,值不许为空串。 */
|
|
3
4
|
export const en: Record<keyof WorkbenchMessages, string> = {
|
|
4
5
|
'brand.name': 'Workbench',
|
|
5
6
|
|
|
@@ -423,20 +424,19 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
423
424
|
'configSync.emptyProviders': 'The workbench has no model provider yet — sync the llm- rows from some profile, or configure one under Models.',
|
|
424
425
|
'configSync.failed': 'The sync did not go through; try again.',
|
|
425
426
|
|
|
426
|
-
'debug.
|
|
427
|
-
'debug.status
|
|
428
|
-
'debug.
|
|
427
|
+
'debug.workspace': 'this workspace',
|
|
428
|
+
'debug.status': 'Debugging “{title}”',
|
|
429
|
+
'debug.status.trouble': 'Debugging needs your attention ({title})',
|
|
430
|
+
'debug.stalled': 'Debugging paused ({title})',
|
|
429
431
|
'debug.act.pause': 'Pause debugging',
|
|
430
432
|
'debug.act.resume': 'Resume debugging',
|
|
431
433
|
'debug.act.stop': 'Stop debugging',
|
|
432
434
|
'debug.act.stop.ask': 'Confirm stop',
|
|
433
|
-
'debug.card.here.title': '
|
|
435
|
+
'debug.card.here.title': 'Debugging “{title}”',
|
|
434
436
|
'debug.card.here.note': 'Do not switch workspaces or touch the panel',
|
|
435
|
-
'debug.card.away.
|
|
436
|
-
'debug.card.away.note': 'Switch to the Workbench DIY workspace',
|
|
437
|
+
'debug.card.away.note': 'Switch back to “{title}”',
|
|
437
438
|
'debug.card.paused.note': 'Paused',
|
|
438
|
-
'debug.card.hidden.note': 'Switch to the Workbench panel',
|
|
439
|
-
'debug.back': 'Back to the workshop screen',
|
|
439
|
+
'debug.card.hidden.note': 'Switch back to the Workbench panel',
|
|
440
440
|
'record.waiting': 'Recording handoff · your turn',
|
|
441
441
|
'record.waiting.note': 'Press "Start recording" on that page, then "Stop recording" when done (it will not switch you back)',
|
|
442
442
|
'record.recording': 'Recording',
|
|
@@ -451,4 +451,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
451
451
|
'approval.record.noUrl': 'no site given (this call gets refused)',
|
|
452
452
|
'approval.record.oneAction': 'one action',
|
|
453
453
|
'approval.record.noSwitch': 'it will not switch you back (the leftbar footer shows the state)',
|
|
454
|
+
'approval.longTask.what': 'Stops',
|
|
455
|
+
'approval.longTask.signal':
|
|
456
|
+
'stopping only sends an interrupt signal — how the platform-side run finishes is up to the module',
|
|
454
457
|
};
|
package/src/i18n/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { en } from './en.js';
|
|
2
2
|
import { zh, type WorkbenchMessages } from './zh.js';
|
|
3
3
|
|
|
4
|
+
/** 外壳自带的两份词表(`zh` 为基准、`en` 同构):`client.tsx` 拿它当合并各套件词表的底座。 */
|
|
4
5
|
export const dictionaries = { zh, en };
|
|
5
6
|
|
|
7
|
+
/** 中文词表的键类型(从 `zh.ts` 转出):`en` 与模块词表都得覆盖同一批键。 */
|
|
6
8
|
export type { WorkbenchMessages };
|
|
7
9
|
|
|
10
|
+
/** 一份按语言分的词表:`zh` / `en` 必须同键,另可带模块自己那份语言。 */
|
|
8
11
|
export interface Dictionaries {
|
|
9
12
|
readonly zh: Readonly<Record<string, string>>;
|
|
10
13
|
readonly en: Readonly<Record<string, string>>;
|
package/src/i18n/merge.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Dictionaries } from './index.js';
|
|
2
2
|
|
|
3
|
+
/** 一个套件交上来的词表:`unit` 是套件 id(也是键前缀),`messages` 缺省表示这次不参与合并。 */
|
|
3
4
|
export interface MessagesContribution {
|
|
4
5
|
readonly unit: string;
|
|
5
6
|
readonly messages?: Dictionaries | undefined;
|
|
@@ -23,6 +24,7 @@ function assertSelfConsistent(unit: string, messages: Dictionaries): void {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** 把各套件的词表合到外壳底座上:键必须以 `<unit>.` 开头、不许撞已有键,违规当场抛错。 */
|
|
26
28
|
export function mergeMessages(
|
|
27
29
|
base: Dictionaries,
|
|
28
30
|
contributions: readonly MessagesContribution[],
|
|
@@ -53,11 +55,13 @@ export function mergeMessages(
|
|
|
53
55
|
return { zh, en };
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/** 一条没合进去的贡献:哪个套件 + 为什么被拒(人话原因)。 */
|
|
56
59
|
export interface MessagesProblem {
|
|
57
60
|
readonly unit: string;
|
|
58
61
|
readonly reason: string;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** 合并结果:合出来的词表 + 被跳过的贡献;`problems` 为空即全部合上了。 */
|
|
61
65
|
export interface MessagesMerge {
|
|
62
66
|
readonly dictionaries: Dictionaries;
|
|
63
67
|
readonly problems: readonly MessagesProblem[];
|
|
@@ -68,6 +72,7 @@ function reasonOf(error: unknown): string {
|
|
|
68
72
|
return typeof error === 'string' ? error : String(error);
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/** 逐份试合、坏的跳过:一份贡献不合规只记进 `problems`,不影响其余套件与已有词表。 */
|
|
71
76
|
export function mergeMessagesSkipping(
|
|
72
77
|
base: Dictionaries,
|
|
73
78
|
contributions: readonly MessagesContribution[],
|
package/src/i18n/zh.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** 外壳自带的中文词表(命名空间 `workbench`):`en` 与它逐键对齐,模块那份在它之上合并。 */
|
|
1
2
|
export const zh = {
|
|
2
3
|
'brand.name': '工作台',
|
|
3
4
|
|
|
@@ -418,20 +419,19 @@ export const zh = {
|
|
|
418
419
|
'configSync.emptyProviders': '工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。',
|
|
419
420
|
'configSync.failed': '同步没成功,再试一次。',
|
|
420
421
|
|
|
421
|
-
'debug.
|
|
422
|
-
'debug.status
|
|
423
|
-
'debug.
|
|
422
|
+
'debug.workspace': '这个工作区',
|
|
423
|
+
'debug.status': '「{title}」调试中',
|
|
424
|
+
'debug.status.trouble': '调试要你看一眼({title})',
|
|
425
|
+
'debug.stalled': '调试已暂停({title})',
|
|
424
426
|
'debug.act.pause': '暂停调试',
|
|
425
427
|
'debug.act.resume': '继续调试',
|
|
426
428
|
'debug.act.stop': '结束调试',
|
|
427
429
|
'debug.act.stop.ask': '确认结束',
|
|
428
|
-
'debug.card.here.title': '
|
|
430
|
+
'debug.card.here.title': '正在调试「{title}」',
|
|
429
431
|
'debug.card.here.note': '请勿切换工作区和操作面板',
|
|
430
|
-
'debug.card.away.
|
|
431
|
-
'debug.card.away.note': '请切换到工作台 DIY 工作区',
|
|
432
|
+
'debug.card.away.note': '请切回「{title}」那一屏',
|
|
432
433
|
'debug.card.paused.note': '已暂停',
|
|
433
|
-
'debug.card.hidden.note': '
|
|
434
|
-
'debug.back': '回到工坊那一屏',
|
|
434
|
+
'debug.card.hidden.note': '请切回工作台面板',
|
|
435
435
|
'record.waiting': '取证交接 · 等你出手',
|
|
436
436
|
'record.waiting.note': '请在那一页按「开始录制」,做完按「结束录制」(录完不替你切回来)',
|
|
437
437
|
'record.recording': '录制中',
|
|
@@ -446,6 +446,9 @@ export const zh = {
|
|
|
446
446
|
'approval.record.noUrl': '没给站点地址(这次会被当场拒)',
|
|
447
447
|
'approval.record.oneAction': '一次操作',
|
|
448
448
|
'approval.record.noSwitch': '录完不会替你切回来(左栏底部会亮一条状态)',
|
|
449
|
+
'approval.longTask.what': '要停的那一趟',
|
|
450
|
+
'approval.longTask.signal': '停了只是让外壳发出中断信号 —— 平台侧那趟活儿的收尾看模块自己',
|
|
449
451
|
} as const;
|
|
450
452
|
|
|
453
|
+
/** 中文词表的键类型:`en` 与模块词表都得覆盖同一批键。 */
|
|
451
454
|
export type WorkbenchMessages = typeof zh;
|
package/src/index.ts
CHANGED
|
@@ -20,8 +20,11 @@ export * from './components/CardBoundary.js';
|
|
|
20
20
|
export * from './components/CardHead.js';
|
|
21
21
|
export * from './components/ChoiceGroup.js';
|
|
22
22
|
export * from './components/Field.js';
|
|
23
|
+
export * from './components/Form.js';
|
|
23
24
|
export * from './components/FormActions.js';
|
|
25
|
+
export * from './components/FormNodes.js';
|
|
24
26
|
export * from './components/FormRows.js';
|
|
27
|
+
export * from './components/FormTypes.js';
|
|
25
28
|
export * from './components/Link.js';
|
|
26
29
|
export * from './components/LongTaskRow.js';
|
|
27
30
|
export * from './components/Note.js';
|
|
@@ -29,6 +32,7 @@ export * from './components/ResourceStates.js';
|
|
|
29
32
|
export * from './components/Rows.js';
|
|
30
33
|
export * from './components/SaveBar.js';
|
|
31
34
|
export * from './components/SelectField.js';
|
|
35
|
+
export * from './components/SelectFieldNodes.js';
|
|
32
36
|
export * from './components/SettingsSection.js';
|
|
33
37
|
export * from './components/Spinner.js';
|
|
34
38
|
export * from './components/Stack.js';
|
|
@@ -43,11 +47,11 @@ export * from './i18n/index.js';
|
|
|
43
47
|
export * from './i18n/merge.js';
|
|
44
48
|
export * from './i18n/zh.js';
|
|
45
49
|
export * from './lib/blocker-action.js';
|
|
46
|
-
export * from './lib/busy.js';
|
|
47
|
-
export * from './lib/form-problem.js';
|
|
48
50
|
export * from './lib/board-fact.js';
|
|
51
|
+
export * from './lib/busy.js';
|
|
49
52
|
export * from './lib/diy-debug.js';
|
|
50
53
|
export * from './lib/diy-record.js';
|
|
54
|
+
export * from './lib/form-problem.js';
|
|
51
55
|
export * from './lib/panel-state.js';
|
|
52
56
|
export * from './lib/react.js';
|
|
53
57
|
export * from './lib/text.js';
|
|
@@ -1,21 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 阻断态取词:i18n 的 `(key) => 文案` 那一半;那几颗钮的「重试中…」等临时说法都从它来。
|
|
3
|
+
*/
|
|
1
4
|
export type BlockerLabelTranslate = (key: string) => string;
|
|
2
5
|
|
|
6
|
+
/**
|
|
7
|
+
* 阻断态上一颗钮的形状:`kind` 定行为(`retry` / `copy` 等),`label` 是它平时的文案。
|
|
8
|
+
*/
|
|
3
9
|
export interface BlockerActionShape {
|
|
4
10
|
readonly kind: string;
|
|
5
11
|
readonly label: string;
|
|
6
12
|
readonly text?: string | undefined;
|
|
7
13
|
}
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 这颗钮此刻的运行态:是否正在重试、哪一支正在跑(按 label 认)、刚复制过哪段文本。
|
|
17
|
+
*/
|
|
9
18
|
export interface BlockerLabelState {
|
|
10
19
|
readonly retrying: boolean;
|
|
11
20
|
readonly running: string | null;
|
|
12
21
|
readonly copied: string | null;
|
|
13
22
|
}
|
|
14
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 这颗钮此刻该不该按成等待态:这一轮忙还没收,或有一支正在跑。
|
|
26
|
+
* @param busy - 这一轮忙是不是还在。
|
|
27
|
+
* @param running - 正在跑的那一支的 label;没有就给 `null`。
|
|
28
|
+
* @returns 该等待给 `true`,否则 `false`。
|
|
29
|
+
*/
|
|
15
30
|
export function blockerPending(busy: boolean, running: string | null): boolean {
|
|
16
31
|
return busy || running !== null;
|
|
17
32
|
}
|
|
18
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 这颗钮此刻该显示什么字:正在重试说「重试中…」,正在跑的那一支说「修复中…」,刚复制过的说「已复制」,其余原样。
|
|
36
|
+
* @param action - 这颗钮的形状(行为 + 平时文案)。
|
|
37
|
+
* @param state - 此刻的运行态。
|
|
38
|
+
* @param t - i18n 取词函数。
|
|
39
|
+
* @returns 要显示的文案;三种临时态都不成立时就是 `action.label`。
|
|
40
|
+
*/
|
|
19
41
|
export function blockerActionLabel(
|
|
20
42
|
action: BlockerActionShape,
|
|
21
43
|
state: BlockerLabelState,
|