@dsh-workbench/shared 0.3.6 → 0.3.7

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 (136) hide show
  1. package/dist/api/context-ref.d.ts +0 -12
  2. package/dist/api/context-ref.js +0 -40
  3. package/dist/api/facts.d.ts +0 -6
  4. package/dist/api/facts.js +0 -8
  5. package/dist/components/ConfirmBar.css +26 -0
  6. package/dist/components/ConfirmBar.d.ts +36 -0
  7. package/dist/components/ConfirmBar.js +26 -0
  8. package/dist/components/Form.d.ts +7 -5
  9. package/dist/components/Form.js +5 -3
  10. package/dist/components/FormNodes.d.ts +17 -13
  11. package/dist/components/FormNodes.js +13 -5
  12. package/dist/components/FormTypes.d.ts +39 -14
  13. package/dist/components/MockStrip.css +11 -1
  14. package/dist/components/StateDot.d.ts +5 -0
  15. package/dist/components/StateDot.js +10 -0
  16. package/dist/config/constants.d.ts +2 -0
  17. package/dist/config/constants.js +2 -0
  18. package/dist/host/__tests__/fakes.d.ts +41 -0
  19. package/dist/host/__tests__/fakes.js +76 -0
  20. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  21. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  22. package/dist/host/board-fact.d.ts +8 -0
  23. package/dist/host/board-fact.js +16 -0
  24. package/dist/host/browser-read.js +2 -1
  25. package/dist/host/context-inject.d.ts +24 -0
  26. package/dist/host/context-inject.js +74 -0
  27. package/dist/host/context-ref.d.ts +13 -0
  28. package/dist/host/context-ref.js +41 -0
  29. package/dist/host/diy-debug.d.ts +80 -0
  30. package/dist/host/diy-debug.js +88 -0
  31. package/dist/host/diy-record.d.ts +135 -0
  32. package/dist/host/diy-record.js +162 -0
  33. package/dist/host/events.d.ts +9 -0
  34. package/dist/host/events.js +70 -0
  35. package/dist/host/facts.d.ts +6 -0
  36. package/dist/host/facts.js +9 -0
  37. package/dist/host/git.js +1 -1
  38. package/dist/host/http.d.ts +2 -2
  39. package/dist/host/http.js +3 -3
  40. package/dist/host/long-task.d.ts +5 -0
  41. package/dist/host/long-task.js +3 -0
  42. package/dist/host/mount-units.d.ts +52 -0
  43. package/dist/host/mount-units.js +186 -0
  44. package/dist/host/opencli-runtime.js +1 -1
  45. package/dist/host/opencli-window.js +1 -1
  46. package/dist/host/opencli.js +2 -1
  47. package/dist/host/panel-state.d.ts +120 -0
  48. package/dist/host/panel-state.js +176 -0
  49. package/dist/host/plugin-source.js +1 -1
  50. package/dist/host/preflight.d.ts +58 -0
  51. package/dist/host/preflight.js +233 -0
  52. package/dist/host/probe-cache.d.ts +6 -0
  53. package/dist/host/probe-cache.js +19 -0
  54. package/dist/host/probe-libs.d.ts +45 -0
  55. package/dist/host/probe-libs.js +61 -0
  56. package/dist/host/probe-units.d.ts +17 -0
  57. package/dist/host/probe-units.js +102 -0
  58. package/dist/host/probe-verdict.d.ts +7 -0
  59. package/dist/host/probe-verdict.js +143 -0
  60. package/dist/host/shell-text.d.ts +13 -0
  61. package/dist/host/shell-text.js +20 -0
  62. package/dist/host/tool-scope.d.ts +32 -0
  63. package/dist/host/tool-scope.js +84 -0
  64. package/dist/host/unit-declaration.d.ts +65 -0
  65. package/dist/host/unit-declaration.js +213 -0
  66. package/dist/host/unit-readiness.d.ts +7 -0
  67. package/dist/host/unit-readiness.js +5 -0
  68. package/dist/host/unit-registry.d.ts +9 -0
  69. package/dist/host/unit-registry.js +33 -0
  70. package/dist/host/version-range.d.ts +2 -0
  71. package/dist/host/version-range.js +73 -0
  72. package/dist/host.d.ts +7 -4
  73. package/dist/host.js +7 -4
  74. package/dist/i18n/en.js +4 -0
  75. package/dist/i18n/index.d.ts +5 -1
  76. package/dist/i18n/zh.d.ts +4 -0
  77. package/dist/i18n/zh.js +4 -0
  78. package/dist/index.d.ts +1 -4
  79. package/dist/index.js +1 -4
  80. package/dist/lib/board-fact.d.ts +0 -7
  81. package/dist/lib/board-fact.js +0 -15
  82. package/dist/lib/diy-record.d.ts +38 -1
  83. package/dist/lib/diy-record.js +66 -0
  84. package/dist/lib/text.d.ts +0 -13
  85. package/dist/lib/text.js +0 -20
  86. package/dist/testing/declaration.js +1 -1
  87. package/package.json +1 -1
  88. package/src/api/__tests__/facts.test.ts +0 -13
  89. package/src/api/context-ref.ts +0 -39
  90. package/src/api/facts.ts +0 -9
  91. package/src/components/ConfirmBar.css +26 -0
  92. package/src/components/ConfirmBar.test.tsx +130 -0
  93. package/src/components/ConfirmBar.tsx +63 -0
  94. package/src/components/Form.test.tsx +15 -0
  95. package/src/components/Form.tsx +7 -5
  96. package/src/components/FormNodes.tsx +50 -26
  97. package/src/components/FormTypes.test.ts +31 -0
  98. package/src/components/FormTypes.ts +46 -15
  99. package/src/components/FormWiring.test.tsx +1 -0
  100. package/src/config/constants.ts +3 -0
  101. package/src/host/__tests__/board-fact.test.ts +47 -0
  102. package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
  103. package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
  104. package/src/host/__tests__/facts.test.ts +13 -0
  105. package/src/host/__tests__/long-task.test.ts +42 -0
  106. package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
  107. package/src/host/__tests__/preflight-route.test.ts +33 -0
  108. package/src/host/__tests__/shell-text.test.ts +44 -0
  109. package/src/host/board-fact.ts +14 -0
  110. package/src/host/browser-read.ts +2 -1
  111. package/src/host/context-ref.ts +40 -0
  112. package/src/{lib → host}/diy-record.ts +95 -1
  113. package/src/host/facts.ts +10 -0
  114. package/src/host/git.ts +1 -1
  115. package/src/host/http.ts +4 -4
  116. package/src/host/long-task.ts +9 -0
  117. package/src/host/opencli-runtime.ts +1 -1
  118. package/src/host/opencli-window.ts +1 -1
  119. package/src/host/opencli.ts +2 -1
  120. package/src/host/plugin-source.ts +1 -1
  121. package/src/host/shell-text.ts +21 -0
  122. package/src/{api → host}/unit-declaration.ts +1 -1
  123. package/src/host.ts +7 -4
  124. package/src/i18n/en.ts +4 -0
  125. package/src/i18n/zh.ts +4 -0
  126. package/src/index.ts +1 -4
  127. package/src/lib/board-fact.test.ts +1 -33
  128. package/src/lib/board-fact.ts +0 -13
  129. package/src/lib/text.test.ts +1 -43
  130. package/src/lib/text.ts +0 -22
  131. package/src/testing/declaration.ts +1 -1
  132. package/dist/lib/lint-probe.d.ts +0 -1
  133. package/dist/lib/lint-probe.js +0 -6
  134. /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
  135. /package/src/{lib → host}/diy-debug.ts +0 -0
  136. /package/src/{lib → host}/panel-state.ts +0 -0
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { preflightHandler } from '../http.js';
3
+
4
+ const SNAPSHOT = { phase: 'ready', steps: [], at: 1 };
5
+
6
+ function handlerWith() {
7
+ const run = vi.fn(async () => SNAPSHOT);
8
+ return { run, handler: preflightHandler({ run, snapshot: () => SNAPSHOT }) };
9
+ }
10
+
11
+ describe('preflightHandler(体检那一拍带什么参数)', () => {
12
+ it('平时(不带 hard)交给宿主自己判新鲜度', async () => {
13
+ const { run, handler } = handlerWith();
14
+
15
+ await handler(new URLSearchParams(''));
16
+
17
+ expect(run).toHaveBeenCalledWith({ hard: false });
18
+ });
19
+
20
+ it('?hard=1 一路传到 run(用户按的那颗「重试」)', async () => {
21
+ const { run, handler } = handlerWith();
22
+
23
+ await handler(new URLSearchParams('hard=1'));
24
+
25
+ expect(run).toHaveBeenCalledWith({ hard: true });
26
+ });
27
+
28
+ it('不管 run 跑没跑完,回的都是当前快照(页面靠 400ms 轮询看进度)', () => {
29
+ const { handler } = handlerWith();
30
+
31
+ expect(handler(new URLSearchParams(''))).toEqual({ ok: true, ...SNAPSHOT });
32
+ });
33
+ });
@@ -0,0 +1,44 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { quoteShellArg, tailLines } from '../shell-text.js';
4
+
5
+ function shellSees(value: string): string {
6
+ return execFileSync('/bin/sh', ['-c', `printf '%s' ${quoteShellArg(value)}`], {
7
+ encoding: 'utf8',
8
+ });
9
+ }
10
+
11
+ describe('quoteShellArg', () => {
12
+ it('普通值原样往返', () => {
13
+ expect(shellSees('example-monorepo')).toBe('example-monorepo');
14
+ });
15
+
16
+ it('含空格的路径不被拆成两个参数', () => {
17
+ expect(shellSees('/Volumes/My Projects/app')).toBe('/Volumes/My Projects/app');
18
+ });
19
+
20
+ it('单引号被正确转义(否则就是注入面)', () => {
21
+ expect(shellSees("it's")).toBe("it's");
22
+ });
23
+
24
+ it('shell 元字符按字面量传递', () => {
25
+ for (const raw of ['$(whoami)', '`id`', 'a; rm -rf /', 'a && b', '*', '"double"', '\\n']) {
26
+ expect(shellSees(raw), raw).toBe(raw);
27
+ }
28
+ });
29
+ });
30
+
31
+ describe('tailLines', () => {
32
+ it('取末尾若干行并去掉首尾空行', () => {
33
+ expect(tailLines('\n a \n b \n', 2)).toBe('a \n b');
34
+ });
35
+
36
+ it('行数不够时全给', () => {
37
+ expect(tailLines('only', 6)).toBe('only');
38
+ });
39
+
40
+ it('空输入给空串', () => {
41
+ expect(tailLines(undefined, 3)).toBe('');
42
+ expect(tailLines(' ', 3)).toBe('');
43
+ });
44
+ });
@@ -0,0 +1,14 @@
1
+ import { insideWorkshop, type BoardFactState, type BoardProbe } from '../lib/board-fact.js';
2
+
3
+ /**
4
+ * 把一次探针读数归成椅面归属。
5
+ * @param probe - 探针读数。
6
+ * @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
7
+ * @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
8
+ */
9
+ export function boardFactOf(probe: BoardProbe, workshop: string | null): BoardFactState {
10
+ if (!probe.workbench) return 'unknown';
11
+ if (!probe.bar || !probe.open || !probe.drawn) return 'hidden';
12
+ if (probe.board === '' || workshop === null) return 'unknown';
13
+ return insideWorkshop(workshop, probe.board) ? 'workshop' : 'other';
14
+ }
@@ -1,5 +1,6 @@
1
1
  import type { DshShell } from '../api/dsh.js';
2
- import { parseJsonLoose, tailLines } from '../lib/text.js';
2
+ import { parseJsonLoose } from '../lib/text.js';
3
+ import { tailLines } from './shell-text.js';
3
4
  import { backgroundLine } from './opencli-window.js';
4
5
  import { runShell } from './shell.js';
5
6
 
@@ -0,0 +1,40 @@
1
+ import { CONTEXT_MARKER_NAME, encodeRef, type ContextRef } from '../api/context-ref.js';
2
+
3
+ const MARKER = new RegExp(`@${CONTEXT_MARKER_NAME}:([a-z][a-z0-9-]*:[A-Za-z0-9._~%#/-]+)`, 'g');
4
+
5
+ const TRAILING_PUNCTUATION = /[.,;:!?]+$/;
6
+
7
+ /**
8
+ * 把 kind:part/part 文本读回引用。
9
+ * @param text 待解析文本。
10
+ * @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
11
+ */
12
+ export function decodeRef(text: string): ContextRef | undefined {
13
+ const at = text.indexOf(':');
14
+ if (at <= 0 || at === text.length - 1) return undefined;
15
+ const kind = text.slice(0, at);
16
+ if (!/^[a-z][a-z0-9-]*$/.test(kind)) return undefined;
17
+ const parts = text.slice(at + 1).split('/').filter((part) => part !== '');
18
+ if (parts.length === 0) return undefined;
19
+ return { kind, parts };
20
+ }
21
+
22
+ /**
23
+ * 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
24
+ * @param text 任意文本;null / undefined 当空串。
25
+ * @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
26
+ */
27
+ export function parseMarkers(text: string): ContextRef[] {
28
+ const seen = new Set<string>();
29
+ const refs: ContextRef[] = [];
30
+ for (const match of String(text ?? '').matchAll(MARKER)) {
31
+ const raw = match[1].replace(TRAILING_PUNCTUATION, '');
32
+ const ref = decodeRef(raw);
33
+ if (ref === undefined) continue;
34
+ const key = encodeRef(ref);
35
+ if (seen.has(key)) continue;
36
+ seen.add(key);
37
+ refs.push(ref);
38
+ }
39
+ return refs;
40
+ }
@@ -17,7 +17,32 @@ export interface RecordExpect {
17
17
  }
18
18
 
19
19
  /**
20
- * 一份活着的录制约定:相位 + 谁的会话 + 哪个工作区 / 单元 + 页面 URL 与标题 + 要找的那一次写 + 落盘文件 + 三个时间戳。
20
+ * 一次「截图」在宿主台账里的那一行:页面上的计数、落图那一刻、落盘路径;没落下来时带 `error` 原文(落成的那条没有它)。
21
+ */
22
+ export interface RecordShot {
23
+ readonly n: number;
24
+ readonly at: number;
25
+ readonly path: string;
26
+ readonly error?: string;
27
+ }
28
+
29
+ /**
30
+ * 某个页面计数已经试过几次:初拍算一次,失败再试一次;计数一换就从头数。
31
+ */
32
+ export interface RecordShotTries {
33
+ readonly n: number;
34
+ readonly tries: number;
35
+ }
36
+
37
+ /**
38
+ * 一张图最多试几次(初拍 + 一次重试):失败如实报过就停,不每拍重试到底。
39
+ */
40
+ export const RECORD_SHOT_TRIES = 2;
41
+
42
+ /**
43
+ * 一份活着的录制约定:相位 + 谁的会话 + 哪个工作区 / 单元 + 页面 URL 与标题 + 要找的那一次写 +
44
+ * 落盘文件 + 三个时间戳,以及两份台账:按计数落图的 `shots`(`shotsSeen` 只记落成的那个计数)
45
+ * 与门铃的 `beats` / `beatMs`(命中几次、最近一次等了多久)。
21
46
  */
22
47
  export interface RecordState {
23
48
  readonly phase: RecordPhase;
@@ -33,6 +58,11 @@ export interface RecordState {
33
58
  readonly startedAt: number;
34
59
  readonly t0: number | null;
35
60
  readonly t1: number | null;
61
+ readonly shots: readonly RecordShot[];
62
+ readonly shotsSeen: number;
63
+ readonly shotTries: RecordShotTries | null;
64
+ readonly beats: number;
65
+ readonly beatMs: number;
36
66
  readonly at: number;
37
67
  }
38
68
 
@@ -95,9 +125,26 @@ export interface RecordStore {
95
125
  begin(pact: RecordPact): RecordMove;
96
126
  started(sessionId: string, at: number, now: number): RecordMove;
97
127
  finished(sessionId: string, at: number, now: number): RecordMove;
128
+ shotTaken(sessionId: string, shot: RecordShot, now: number): RecordMove;
129
+ shotFailed(sessionId: string, shot: RecordShot, now: number): RecordMove;
130
+ beat(sessionId: string, ms: number, now: number): RecordMove;
98
131
  stop(sessionId: string, now: number): RecordMove;
99
132
  }
100
133
 
134
+ /**
135
+ * 这一拍该不该落图:页面计数涨过 `shotsSeen` 才算;同一个计数已经试满就先放下,等他自己再按。
136
+ * @param state - 当前有效的那份约定。
137
+ * @param pageShots - 页面浮条上那个「已拍」计数。
138
+ * @returns 该落的那个计数;不该落给 `null`(没涨 / 试满了 / 不是个正经数)。
139
+ */
140
+ export function shotDueOf(state: RecordState, pageShots: number): number | null {
141
+ if (!Number.isFinite(pageShots)) return null;
142
+ const n = Math.floor(pageShots);
143
+ if (n <= state.shotsSeen) return null;
144
+ if (state.shotTries !== null && state.shotTries.n === n && state.shotTries.tries >= RECORD_SHOT_TRIES) return null;
145
+ return n;
146
+ }
147
+
101
148
  function alive(state: RecordState | null, now: number): RecordState | null {
102
149
  if (state === null) return null;
103
150
  return now - state.at <= RECORD_STALE_MS ? state : null;
@@ -118,6 +165,19 @@ export function createRecordStore(): RecordStore {
118
165
 
119
166
  const live = (now: number): RecordState | null => alive(held, now);
120
167
 
168
+ const withShot = (found: RecordState, shot: RecordShot, now: number, error: string | undefined): RecordState => {
169
+ const row: RecordShot = error === undefined ? shot : { n: shot.n, at: shot.at, path: shot.path, error };
170
+ const kept = found.shots.filter((one) => one.n !== shot.n);
171
+ const tries = found.shotTries !== null && found.shotTries.n === shot.n ? found.shotTries.tries : 0;
172
+ return {
173
+ ...found,
174
+ shots: [...kept, row].sort((one, two) => one.n - two.n),
175
+ shotsSeen: error === undefined ? Math.max(found.shotsSeen, shot.n) : found.shotsSeen,
176
+ shotTries: error === undefined ? null : { n: shot.n, tries: tries + 1 },
177
+ at: now,
178
+ };
179
+ };
180
+
121
181
  return {
122
182
  live,
123
183
 
@@ -140,6 +200,11 @@ export function createRecordStore(): RecordStore {
140
200
  startedAt: pact.now,
141
201
  t0: null,
142
202
  t1: null,
203
+ shots: [],
204
+ shotsSeen: 0,
205
+ shotTries: null,
206
+ beats: 0,
207
+ beatMs: 0,
143
208
  at: pact.now,
144
209
  };
145
210
  held = state;
@@ -165,6 +230,35 @@ export function createRecordStore(): RecordStore {
165
230
  return { ok: true, state };
166
231
  },
167
232
 
233
+ shotTaken(sessionId, shot, now) {
234
+ const found = ownerOf(held, sessionId, now);
235
+ if (typeof found === 'string') return { ok: false, code: found };
236
+ const state = withShot(found, shot, now, undefined);
237
+ held = state;
238
+ return { ok: true, state };
239
+ },
240
+
241
+ shotFailed(sessionId, shot, now) {
242
+ const found = ownerOf(held, sessionId, now);
243
+ if (typeof found === 'string') return { ok: false, code: found };
244
+ const state = withShot(found, shot, now, shot.error === undefined ? '落图失败' : shot.error);
245
+ held = state;
246
+ return { ok: true, state };
247
+ },
248
+
249
+ beat(sessionId, ms, now) {
250
+ const found = ownerOf(held, sessionId, now);
251
+ if (typeof found === 'string') return { ok: false, code: found };
252
+ const state: RecordState = {
253
+ ...found,
254
+ beats: found.beats + 1,
255
+ beatMs: Math.max(0, Math.floor(ms)),
256
+ at: now,
257
+ };
258
+ held = state;
259
+ return { ok: true, state };
260
+ },
261
+
168
262
  stop(sessionId, now) {
169
263
  const found = ownerOf(held, sessionId, now);
170
264
  if (typeof found === 'string') return { ok: false, code: found };
package/src/host/facts.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { UnitManifest } from '../api/unit.js';
2
+ import { FACT_SCOPE_SUFFIX } from '../api/facts.js';
2
3
 
3
4
  export interface FactChange {
4
5
  readonly rev: number;
@@ -67,3 +68,12 @@ export function wroteNow(result: unknown): boolean | undefined {
67
68
  const wrote = (value as { wrote?: unknown } | null)?.wrote;
68
69
  return typeof wrote === 'boolean' ? wrote : undefined;
69
70
  }
71
+
72
+ /**
73
+ * 由单元 id 拼出它那一族事实的范围名。
74
+ * @param unit 单元 id,不带后缀。
75
+ * @returns <unit>* —— 永远是范围名,不是某一条事实。
76
+ */
77
+ export function unitFactScope(unit: string): string {
78
+ return `${unit}${FACT_SCOPE_SUFFIX}`;
79
+ }
package/src/host/git.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DshShell } from '../api/dsh.js';
2
- import { quoteShellArg, tailLines } from '../lib/text.js';
2
+ import { quoteShellArg, tailLines } from './shell-text.js';
3
3
  import { runShell } from './shell.js';
4
4
 
5
5
  export type OriginRead =
package/src/host/http.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { DshHostContext, DshRequest, DshResponse } from '../api/dsh.js';
2
- import { unitFactScope } from '../api/facts.js';
3
- import { parseCardsReport, parseCrashReport, type PanelStateStore } from '../lib/panel-state.js';
2
+ import { unitFactScope } from './facts.js';
3
+ import { parseCardsReport, parseCrashReport, type PanelStateStore } from './panel-state.js';
4
4
  import { ACTIONS_PREFIX, API_PREFIX, DIY_DEBUG_PATH, DIY_RECORD_PATH, PANEL_STATE_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
5
5
  import {
6
6
  parseProvisionBody,
@@ -228,11 +228,11 @@ export function changesHandler(log: {
228
228
  }
229
229
 
230
230
  export function preflightHandler(preflight: {
231
- run(options?: { force?: boolean }): Promise<unknown>;
231
+ run(options?: { hard?: boolean }): Promise<unknown>;
232
232
  snapshot(): unknown;
233
233
  }): RouteHandler {
234
234
  return (search) => {
235
- void preflight.run({ force: flag(search, 'force') });
235
+ void preflight.run({ hard: flag(search, 'hard') });
236
236
  return { ok: true, ...(preflight.snapshot() as Record<string, unknown>) };
237
237
  };
238
238
  }
@@ -48,11 +48,16 @@ export interface LongTaskDeps<S extends LongTaskBase> {
48
48
  readonly recentMs?: number | undefined;
49
49
  }
50
50
 
51
+ /**
52
+ * 一个单元的长任务注册表:`current(workspace)` 是面板那一行要的投影,`live()` 是外壳侧栏那颗
53
+ * loading 点要的(**不分工作区、不分目标**,把现在活着的每一趟交出来),`stop` 只把中断信号转给生产方。
54
+ */
51
55
  export interface LongTaskRegistry<S extends LongTaskBase> {
52
56
  start(options: LongTaskStart): S;
53
57
  snapshot(id: string): S | null;
54
58
  active(workspace: string, target: string): S | null;
55
59
  current(workspace: string): readonly Readonly<Record<string, unknown>>[];
60
+ live(): readonly S[];
56
61
  stop(id: string, by?: string): boolean;
57
62
  onChange(listener: (change: LongTaskChange<S>) => void): () => void;
58
63
  }
@@ -113,6 +118,10 @@ export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDep
113
118
  .map((snapshot) => ({ ...deps.view(snapshot as S), id: snapshot.id }));
114
119
  },
115
120
 
121
+ live() {
122
+ return [...runs.values()].filter((snapshot) => LIVE.has(snapshot.status)) as S[];
123
+ },
124
+
116
125
  start(options) {
117
126
  const running = this.active(options.workspace, options.target);
118
127
  if (running !== null) return running;
@@ -1,6 +1,6 @@
1
1
  import { OPENCLI_PIN } from '../config/constants.js';
2
2
  import type { UnitLibNeed } from '../api/unit.js';
3
- import { quoteShellArg } from '../lib/text.js';
3
+ import { quoteShellArg } from './shell-text.js';
4
4
 
5
5
  import { dshHome } from './data-root.js';
6
6
 
@@ -1,4 +1,4 @@
1
- import { quoteShellArg } from '../lib/text.js';
1
+ import { quoteShellArg } from './shell-text.js';
2
2
 
3
3
  const OPENCLI_WINDOW_ENV = 'OPENCLI_WINDOW=background';
4
4
 
@@ -1,6 +1,7 @@
1
1
  import type { DshShell } from '../api/dsh.js';
2
2
  import { OPENCLI_MAX_CONCURRENT, OPENCLI_TIMEOUT_MS } from '../config/constants.js';
3
- import { parseJsonLoose, quoteShellArg, tailLines } from '../lib/text.js';
3
+ import { parseJsonLoose } from '../lib/text.js';
4
+ import { quoteShellArg, tailLines } from './shell-text.js';
4
5
  import { helpUrlOf, parseCliErrorEnvelope } from './cli-error.js';
5
6
  import { backgroundPrefix } from './opencli-window.js';
6
7
  import { runShell } from './shell.js';
@@ -1,5 +1,5 @@
1
1
  import type { PluginSource } from '../api/unit.js';
2
- import { quoteShellArg } from '../lib/text.js';
2
+ import { quoteShellArg } from './shell-text.js';
3
3
  import { npmCacheDir, toolchainOpencliDir, vendorPeerLink, vendorPrefix } from '../host/opencli-runtime.js';
4
4
 
5
5
  export type PluginSourceProblem =
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
3
+ * @param value - 任意值;不是字符串的先 `String()`。
4
+ * @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
5
+ */
6
+ export function quoteShellArg(value: unknown): string {
7
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
8
+ }
9
+
10
+ /**
11
+ * 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
12
+ * @param text - 原文;`null` / `undefined` 当空串。
13
+ * @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
14
+ * @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
15
+ */
16
+ export function tailLines(text: string | null | undefined, lines = 6): string {
17
+ const all = String(text ?? '')
18
+ .trim()
19
+ .split('\n');
20
+ return all.slice(-lines).join('\n');
21
+ }
@@ -1,4 +1,4 @@
1
- import type { UnitEnvNeeds, UnitHostNeeds, UnitManifest } from './unit.js';
1
+ import type { UnitEnvNeeds, UnitHostNeeds, UnitManifest } from '../api/unit.js';
2
2
 
3
3
  /**
4
4
  * 声明协议当前版本;模块清单的 dsh.workbench.apiVersion 要写这个整数。
package/src/host.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  export * from './config/constants.js';
2
2
  export * from './api/context-ref.js';
3
3
  export * from './api/unit.js';
4
- export * from './api/unit-declaration.js';
5
4
  export * from './api/facts.js';
6
5
  export * from './api/long-task.js';
7
6
  export * from './lib/text.js';
8
7
  export * from './lib/ui.js';
9
- export * from './lib/panel-state.js';
10
8
  export * from './lib/board-fact.js';
11
- export * from './lib/diy-debug.js';
12
- export * from './lib/diy-record.js';
13
9
  export * from './api/blocker-table.js';
10
+ export * from './host/board-fact.js';
14
11
  export * from './host/browser-find.js';
15
12
  export * from './host/browser-read.js';
16
13
  export * from './host/cli-error.js';
14
+ export * from './host/context-ref.js';
17
15
  export * from './host/data-root.js';
16
+ export * from './host/diy-debug.js';
17
+ export * from './host/diy-record.js';
18
18
  export * from './host/facts.js';
19
19
  export * from './host/git.js';
20
20
  export * from './host/http.js';
@@ -22,12 +22,15 @@ export * from './host/long-task.js';
22
22
  export * from './host/opencli-runtime.js';
23
23
  export * from './host/opencli-window.js';
24
24
  export * from './host/opencli.js';
25
+ export * from './host/panel-state.js';
25
26
  export * from './host/plugin-ledger.js';
26
27
  export * from './host/plugin-source.js';
27
28
  export * from './host/provision.js';
29
+ export * from './host/shell-text.js';
28
30
  export * from './host/shell.js';
29
31
  export * from './host/tool-approval.js';
30
32
  export * from './host/tool-kit.js';
33
+ export * from './host/unit-declaration.js';
31
34
  export * from './host/unit-face.js';
32
35
  export * from './host/unit-libs.js';
33
36
  export * from './host/value-handoff.js';
package/src/i18n/en.ts CHANGED
@@ -40,6 +40,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
40
40
  'session.actions': 'Actions for {name}',
41
41
  'status.idle': 'Idle',
42
42
  'status.running': 'Running',
43
+ 'status.longTask': 'Long task running',
43
44
  'status.waitingApproval': 'Waiting for approval',
44
45
  'status.planReview': 'Plan awaiting review',
45
46
  'status.waitingAnswer': 'Waiting for your answer',
@@ -53,6 +54,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
53
54
  'project.attention.warning.other': '{n} sessions waiting for you',
54
55
  'project.attention.running.one': '1 session running',
55
56
  'project.attention.running.other': '{n} sessions running',
57
+ 'project.attention.longTask.one': '1 long task running',
58
+ 'project.attention.longTask.other': '{n} long tasks running',
56
59
  'project.attention.done.one': '1 session finished, unread',
57
60
  'project.attention.done.other': '{n} sessions finished, unread',
58
61
  'project.attention.state.warning': 'Approval',
@@ -441,6 +444,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
441
444
  'record.waiting.note': 'Press "Start recording" on that page, then "Stop recording" when done (it will not switch you back)',
442
445
  'record.recording': 'Recording',
443
446
  'record.recording.note': 'Press "Stop recording" when done; if the bar is gone press Halt so the agent grabs it first',
447
+ 'record.recording.shots': '; to keep a shot of this moment press "Screenshot" on the bar ({n} taken)',
444
448
  'record.finished': 'Recording finished',
445
449
  'record.finished.note': 'This run is done — the agent will grab it and wrap up on its own',
446
450
  'record.act.cancel': 'Cancel',
package/src/i18n/zh.ts CHANGED
@@ -38,6 +38,7 @@ export const zh = {
38
38
  'session.actions': '{name} 的操作',
39
39
  'status.idle': '空闲',
40
40
  'status.running': '进行中',
41
+ 'status.longTask': '长任务在跑',
41
42
  'status.waitingApproval': '等待审批',
42
43
  'status.planReview': '计划待审',
43
44
  'status.waitingAnswer': '等待回答',
@@ -51,6 +52,8 @@ export const zh = {
51
52
  'project.attention.warning.other': '{n} 个会话在等你点头',
52
53
  'project.attention.running.one': '1 个会话在跑',
53
54
  'project.attention.running.other': '{n} 个会话在跑',
55
+ 'project.attention.longTask.one': '1 个长任务在跑',
56
+ 'project.attention.longTask.other': '{n} 个长任务在跑',
54
57
  'project.attention.done.one': '1 个会话跑完没看',
55
58
  'project.attention.done.other': '{n} 个会话跑完没看',
56
59
  'project.attention.state.warning': '待审批',
@@ -436,6 +439,7 @@ export const zh = {
436
439
  'record.waiting.note': '请在那一页按「开始录制」,做完按「结束录制」(录完不替你切回来)',
437
440
  'record.recording': '录制中',
438
441
  'record.recording.note': '做完按「结束录制」;浮条没了按「停」,让 agent 先取再收',
442
+ 'record.recording.shots': ';想留一张现场就按浮条上的「截图」(已拍 {n} 张)',
439
443
  'record.finished': '录制完成',
440
444
  'record.finished.note': '这一段录完了 —— agent 会自己来取并收工,不用你点',
441
445
  'record.act.cancel': '取消',
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@ export * from './api/facts.js';
7
7
  export * from './api/host-write.js';
8
8
  export * from './api/long-task.js';
9
9
  export * from './api/unit-board.js';
10
- export * from './api/unit-declaration.js';
11
10
  export * from './api/unit.js';
12
11
  export * from './api/workbench-client.js';
13
12
  export * from './api/workbench-contract.js';
@@ -19,6 +18,7 @@ export * from './components/CardAnchor.js';
19
18
  export * from './components/CardBoundary.js';
20
19
  export * from './components/CardHead.js';
21
20
  export * from './components/ChoiceGroup.js';
21
+ export * from './components/ConfirmBar.js';
22
22
  export * from './components/Field.js';
23
23
  export * from './components/Form.js';
24
24
  export * from './components/FormActions.js';
@@ -49,10 +49,7 @@ export * from './i18n/zh.js';
49
49
  export * from './lib/blocker-action.js';
50
50
  export * from './lib/board-fact.js';
51
51
  export * from './lib/busy.js';
52
- export * from './lib/diy-debug.js';
53
- export * from './lib/diy-record.js';
54
52
  export * from './lib/form-problem.js';
55
- export * from './lib/panel-state.js';
56
53
  export * from './lib/react.js';
57
54
  export * from './lib/text.js';
58
55
  export * from './lib/ui.js';
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { boardFactOf, boardSituation, insideWorkshop, type BoardProbe } from './board-fact.js';
2
+ import { boardSituation, insideWorkshop, type BoardProbe } from './board-fact.js';
3
3
 
4
4
  const WORKSHOP = '/w/workbench/diy';
5
5
  const FL0XIN = '/w/floxin-app';
@@ -16,38 +16,6 @@ const probe = (over: Partial<BoardProbe> = {}): BoardProbe => ({
16
16
 
17
17
  const gone = { bar: false, board: '', open: false, drawn: false } as const;
18
18
 
19
- describe('板事实:一份判据,两边共用', () => {
20
- it('★★ 画着的就是工坊那块板(含子路径)⇒ workshop', () => {
21
- expect(boardFactOf(probe(), WORKSHOP)).toBe('workshop');
22
- expect(boardFactOf(probe({ board: `${WORKSHOP}/packages` }), WORKSHOP)).toBe('workshop');
23
- });
24
-
25
- it('★★ 画的是别的项目的板 ⇒ other(前缀像但没落在里面也算 other)', () => {
26
- expect(boardFactOf(probe({ board: '/w/project' }), WORKSHOP)).toBe('other');
27
- expect(boardFactOf(probe({ board: `${WORKSHOP}-other` }), WORKSHOP)).toBe('other');
28
- });
29
-
30
- it('★★ 那块板没画在屏上 ⇒ hidden:不挂、没声明开着、量出来不可见,三条里任一条不成立都算', () => {
31
- expect(boardFactOf(probe({ bar: false }), WORKSHOP)).toBe('hidden');
32
- expect(boardFactOf(probe({ open: false }), WORKSHOP)).toBe('hidden');
33
- expect(boardFactOf(probe({ drawn: false }), WORKSHOP)).toBe('hidden');
34
- });
35
-
36
- it('★★ 工作台没答话(旧外壳 / 夹具页)⇒ unknown:放行,但不许当成"就是工坊板"', () => {
37
- expect(boardFactOf(probe({ workbench: false }), WORKSHOP)).toBe('unknown');
38
- });
39
-
40
- it('★ 画着板但说不清是哪块、或不知道工坊在哪 ⇒ unknown', () => {
41
- expect(boardFactOf(probe({ board: '' }), WORKSHOP)).toBe('unknown');
42
- expect(boardFactOf(probe(), null)).toBe('unknown');
43
- });
44
-
45
- it('★★ 顺序要紧:没画在屏上,就别去猜是哪块板', () => {
46
- expect(boardFactOf(probe({ board: WORKSHOP, drawn: false }), WORKSHOP)).toBe('hidden');
47
- expect(boardFactOf(probe({ workbench: false, bar: false }), WORKSHOP)).toBe('unknown');
48
- });
49
- });
50
-
51
19
  describe('干不了活是为什么:先看人在不在工坊,再看板画没画在屏上', () => {
52
20
  it('★★ 在位 ⇒ ok(跟 boardFactOf === "workshop" 是同一个条件)', () => {
53
21
  expect(boardSituation(probe(), WORKSHOP)).toBe('ok');
@@ -41,19 +41,6 @@ export function insideWorkshop(root: string, target: string): boolean {
41
41
  return target.startsWith(base);
42
42
  }
43
43
 
44
- /**
45
- * 把一次探针读数归成椅面归属。
46
- * @param probe - 探针读数。
47
- * @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
48
- * @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
49
- */
50
- export function boardFactOf(probe: BoardProbe, workshop: string | null): BoardFactState {
51
- if (!probe.workbench) return 'unknown';
52
- if (!probe.bar || !probe.open || !probe.drawn) return 'hidden';
53
- if (probe.board === '' || workshop === null) return 'unknown';
54
- return insideWorkshop(workshop, probe.board) ? 'workshop' : 'other';
55
- }
56
-
57
44
  /**
58
45
  * 把探针读数归成左栏那句人话的档位。
59
46
  * @param probe - 探针读数。