@openprd/cli 0.1.18 → 0.1.21

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 (118) hide show
  1. package/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
  2. package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
  3. package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
  4. package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
  6. package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
  7. package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
  8. package/.openprd/changes/openprd-generated-change/.openprd.yaml +2 -0
  9. package/.openprd/changes/openprd-generated-change/design.md +50 -0
  10. package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
  11. package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
  12. package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
  13. package/.openprd/changes/openprd-generated-change/tasks.md +357 -0
  14. package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
  15. package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
  16. package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
  17. package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
  18. package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
  19. package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
  20. package/.openprd/design/active/asset-spec.md +19 -14
  21. package/.openprd/design/active/direction-plan.md +19 -3
  22. package/.openprd/design/active/facts-sheet.md +16 -7
  23. package/.openprd/design/active/image-preflight.md +6 -5
  24. package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
  25. package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
  26. package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
  27. package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
  28. package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
  29. package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
  30. package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
  31. package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
  32. package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
  33. package/.openprd/design/active/selected-direction.md +23 -9
  34. package/.openprd/engagements/active/control-plane-architecture.json +203 -0
  35. package/.openprd/engagements/active/control-plane-intake.json +418 -0
  36. package/.openprd/engagements/active/prd.md +186 -100
  37. package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
  38. package/.openprd/i18n-config.json +12 -0
  39. package/.openprd/ledger/events.jsonl +6 -0
  40. package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
  41. package/.openprd/manifest.json +21 -0
  42. package/AGENTS.md +1 -0
  43. package/README.md +22 -0
  44. package/README_EN.md +27 -0
  45. package/package.json +1 -1
  46. package/skills/openprd-frontend-design/SKILL.md +16 -0
  47. package/skills/openprd-harness/SKILL.md +1 -1
  48. package/skills/openprd-quality/SKILL.md +1 -1
  49. package/skills/openprd-requirement-intake/SKILL.md +1 -1
  50. package/skills/openprd-shared/SKILL.md +1 -1
  51. package/src/adapters/adapter-spi.js +193 -0
  52. package/src/adapters/capability-envelope.js +98 -0
  53. package/src/adapters/event-normalizer.js +55 -0
  54. package/src/adapters/index.js +4 -0
  55. package/src/adapters/install-safety.js +249 -0
  56. package/src/agent-canonical-content.js +6 -2
  57. package/src/agent-integration.js +147 -42
  58. package/src/canvas-app-client-app.js +838 -0
  59. package/src/canvas-app-client-helpers.js +1138 -0
  60. package/src/canvas-app-styles.js +898 -0
  61. package/src/canvas-app.html.js +6 -2781
  62. package/src/canvas-binding.js +414 -0
  63. package/src/canvas-codex-relay.js +430 -0
  64. package/src/canvas-i18n.js +8 -0
  65. package/src/canvas-scene.js +377 -0
  66. package/src/canvas-session-store.js +148 -0
  67. package/src/canvas-workspace.js +59 -1066
  68. package/src/cli/args.js +63 -4
  69. package/src/cli/gate-print.js +17 -0
  70. package/src/cli/quality-commands.js +18 -0
  71. package/src/cli/quality-print.js +10 -0
  72. package/src/cli/runtime-print.js +24 -0
  73. package/src/codex-hook-runner-template.mjs +87 -77
  74. package/src/codex-runtime.js +48 -5
  75. package/src/context/cache.js +245 -0
  76. package/src/context/compiler.js +438 -0
  77. package/src/context/constants.js +30 -0
  78. package/src/context/index.js +39 -0
  79. package/src/context/redaction.js +84 -0
  80. package/src/context/stable.js +69 -0
  81. package/src/context/telemetry.js +42 -0
  82. package/src/dev-standards.js +57 -0
  83. package/src/fleet.js +112 -95
  84. package/src/gates/index.js +2 -0
  85. package/src/gates/scoped-gates.js +256 -0
  86. package/src/gates/store.js +126 -0
  87. package/src/gates/workspace.js +41 -0
  88. package/src/html-artifacts.js +725 -28
  89. package/src/kernel/atomic-store.js +299 -0
  90. package/src/kernel/event-envelope.js +166 -0
  91. package/src/kernel/index.js +4 -0
  92. package/src/kernel/project-ledger.js +467 -0
  93. package/src/kernel/project-manifest.js +205 -0
  94. package/src/knowledge-v3/index.js +1 -0
  95. package/src/knowledge-v3/lifecycle.js +290 -0
  96. package/src/knowledge.js +14 -7
  97. package/src/openprd.js +71 -2
  98. package/src/review-model.js +413 -0
  99. package/src/review-presentation.js +1 -1
  100. package/src/run-harness.js +431 -37
  101. package/src/runtime/cli_runtime_README.md +28 -0
  102. package/src/runtime/errors.js +66 -0
  103. package/src/runtime/index.js +44 -0
  104. package/src/runtime/lane-schema.js +141 -0
  105. package/src/runtime/lane-store.js +279 -0
  106. package/src/runtime/task-runtime.js +449 -0
  107. package/src/runtime/workspace.js +179 -0
  108. package/src/runtime/write-set.js +206 -0
  109. package/src/session-binding.js +16 -3
  110. package/src/session-registry.js +59 -1
  111. package/src/upgrade/fleet-mutation.js +166 -0
  112. package/src/upgrade/fleet-transaction.js +398 -0
  113. package/src/upgrade/transaction-store.js +416 -0
  114. package/src/visual-compare-core.js +66 -27
  115. package/src/visual-compare.js +18 -12
  116. package/src/workspace-core.js +109 -7
  117. package/src/workspace-registry.js +39 -1
  118. package/src/workspace-workflow.js +18 -15
@@ -0,0 +1,838 @@
1
+ /*
2
+ * Core purpose
3
+ * React CanvasApp component and boot logic for the `openprd canvas` app.
4
+ *
5
+ * Positioning
6
+ * Raw inline <script type="module"> payload (part 2 of 2), extracted from
7
+ * canvas-app.html.js. Concatenated after canvas-app-client-helpers.js inside
8
+ * the same module script; the two parts share one lexical scope.
9
+ */
10
+ export const CANVAS_APP_CLIENT_APP = `
11
+ function CanvasApp() {
12
+ const [initialData, setInitialData] = React.useState(null);
13
+ const [loadError, setLoadError] = React.useState(null);
14
+ const [locale, setLocale] = React.useState(initialLocale);
15
+ const [selectedCount, setSelectedCount] = React.useState(0);
16
+ const [selectedImageCount, setSelectedImageCount] = React.useState(0);
17
+ const [isSending, setIsSending] = React.useState(false);
18
+ const [isExporting, setIsExporting] = React.useState(false);
19
+ const [isDownloadingSelection, setIsDownloadingSelection] = React.useState(false);
20
+ const [isOpeningFolder, setIsOpeningFolder] = React.useState(false);
21
+ const [actionStatus, setActionStatus] = React.useState(null);
22
+ const [composer, setComposer] = React.useState(null);
23
+ const [imageComposer, setImageComposer] = React.useState(null);
24
+ const [isRequestingImage, setIsRequestingImage] = React.useState(false);
25
+ const [libraryItems, setLibraryItems] = React.useState([]);
26
+ const [libraryPreviews, setLibraryPreviews] = React.useState({});
27
+ const [guide, setGuide] = React.useState(null);
28
+ const [isImportingDirections, setIsImportingDirections] = React.useState(false);
29
+ const latestHandoffSignalSeqRef = React.useRef(0);
30
+ const t = messagesFor(locale);
31
+ const aiTemplates = React.useMemo(() => imageTemplateOptions(t), [locale]);
32
+
33
+ React.useEffect(() => {
34
+ syncSelectionState = (appState, elements) => {
35
+ setSelectedCount(selectedCountFromAppState(appState));
36
+ setSelectedImageCount(selectedImageCountFromAppState(appState, elements));
37
+ };
38
+ return () => {
39
+ syncSelectionState = () => {};
40
+ };
41
+ }, []);
42
+
43
+ React.useEffect(() => {
44
+ const shouldApplyHandoffSignal = (op) => {
45
+ const seq = Number(op.seq || 0);
46
+ if (seq > 0 && seq < latestHandoffSignalSeqRef.current) {
47
+ return false;
48
+ }
49
+ if (seq > 0) {
50
+ latestHandoffSignalSeqRef.current = seq;
51
+ }
52
+ return true;
53
+ };
54
+ syncCodexRelayStatus = (op) => {
55
+ if (!shouldApplyHandoffSignal(op)) {
56
+ return;
57
+ }
58
+ if (op.dryRun) {
59
+ setActionStatus(null);
60
+ return;
61
+ }
62
+ if (op.status === "completed" || op.status === "started") {
63
+ setActionStatus({ type: "success", message: t.sentBodyWithRelay });
64
+ } else if (["queued", "starting", "resuming", "connecting"].includes(op.status)) {
65
+ setActionStatus({ type: "busy", message: t.sentBodyPending });
66
+ } else if (op.status === "failed" || op.status === "timeout" || op.status === "closed") {
67
+ setActionStatus({ type: "error", message: op.error || t.agentBridgeFailed });
68
+ }
69
+ };
70
+ syncAgentBridgeStatus = (op) => {
71
+ if (!shouldApplyHandoffSignal(op)) {
72
+ return;
73
+ }
74
+ if (op.status === "sent") {
75
+ setActionStatus({ type: "success", message: t.agentBridgeSent });
76
+ } else if (op.status === "failed") {
77
+ setActionStatus({ type: "error", message: op.reason || t.agentBridgeFailed });
78
+ } else if (op.status === "pending" || op.status === "claimed") {
79
+ setActionStatus({ type: "busy", message: agentBridgePendingMessage(t, op.status) });
80
+ }
81
+ };
82
+ return () => {
83
+ syncCodexRelayStatus = () => {};
84
+ syncAgentBridgeStatus = () => {};
85
+ };
86
+ }, [t]);
87
+
88
+ React.useEffect(() => {
89
+ document.documentElement.lang = locale;
90
+ try {
91
+ window.localStorage.setItem(localeConfig.storageKey, locale);
92
+ } catch {}
93
+ }, [locale]);
94
+
95
+ React.useEffect(() => {
96
+ let mounted = true;
97
+ Promise.all([
98
+ fetchJson("/api/scene"),
99
+ fetchJson("/api/library").catch(() => ({ libraryItems: [] })),
100
+ ])
101
+ .then(([scene, library]) => {
102
+ if (mounted) {
103
+ const libraryItems = libraryItemsFromPayload(library);
104
+ const normalizedScene = normalizeInitialScene(scene);
105
+ setInitialData({
106
+ ...normalizedScene,
107
+ libraryItems,
108
+ });
109
+ setLibraryItems(libraryItems);
110
+ latestElements = normalizedScene.elements;
111
+ latestAppState = normalizedScene.appState;
112
+ latestFiles = normalizedScene.files;
113
+ setSelectedCount(selectedCountFromAppState(normalizedScene.appState));
114
+ setSelectedImageCount(selectedImageCountFromAppState(normalizedScene.appState, normalizedScene.elements));
115
+ if (scene?.appState?.viewBackgroundColor !== normalizedScene.appState.viewBackgroundColor) {
116
+ fetchJson("/api/scene", {
117
+ method: "POST",
118
+ body: JSON.stringify({
119
+ type: "openprd.canvas.scene.v1",
120
+ elements: normalizedScene.elements,
121
+ appState: stripAppState(normalizedScene.appState),
122
+ files: normalizedScene.files,
123
+ savedAt: new Date().toISOString(),
124
+ }),
125
+ }).catch((error) => {
126
+ console.warn("OpenPrd canvas background migration failed", error);
127
+ });
128
+ }
129
+ }
130
+ })
131
+ .catch((error) => {
132
+ if (mounted) {
133
+ setLoadError(error);
134
+ }
135
+ });
136
+ return () => {
137
+ mounted = false;
138
+ window.clearInterval(pollTimer);
139
+ };
140
+ }, []);
141
+
142
+ React.useEffect(() => {
143
+ pollTimer = window.setInterval(pollOperations, 1400);
144
+ pollOperations();
145
+ return () => window.clearInterval(pollTimer);
146
+ }, []);
147
+
148
+ React.useEffect(() => {
149
+ let mounted = true;
150
+ const loadGuide = () => {
151
+ fetchJson("/api/guide")
152
+ .then((result) => {
153
+ if (mounted && result && result.ok && result.currentStage) {
154
+ setGuide(result);
155
+ }
156
+ })
157
+ .catch(() => {});
158
+ };
159
+ loadGuide();
160
+ const timer = window.setInterval(loadGuide, 30000);
161
+ return () => {
162
+ mounted = false;
163
+ window.clearInterval(timer);
164
+ };
165
+ }, []);
166
+
167
+ async function handleImportReviewDirections() {
168
+ if (isImportingDirections) {
169
+ return;
170
+ }
171
+ setIsImportingDirections(true);
172
+ try {
173
+ const result = await importReviewDirectionsToCanvas(t);
174
+ setActionStatus({ type: "success", message: formatMessage(t.reviewDirectionsImported, { count: result.count }) });
175
+ } catch (error) {
176
+ setActionStatus({
177
+ type: "error",
178
+ message: error?.code === "empty" ? t.reviewDirectionsEmpty : t.reviewDirectionsFailed,
179
+ });
180
+ } finally {
181
+ setIsImportingDirections(false);
182
+ }
183
+ }
184
+
185
+ React.useEffect(() => {
186
+ hideNativeExcalidrawChrome();
187
+ const observer = new MutationObserver(hideNativeExcalidrawChrome);
188
+ const root = document.querySelector(".canvas-wrap");
189
+ if (root) {
190
+ observer.observe(root, { childList: true, subtree: true });
191
+ }
192
+ return () => observer.disconnect();
193
+ }, [initialData, locale]);
194
+
195
+ React.useEffect(() => {
196
+ let cancelled = false;
197
+ async function buildPreviews() {
198
+ if (!initialData || !exportToCanvas || libraryItems.length === 0) {
199
+ setLibraryPreviews({});
200
+ return;
201
+ }
202
+ const previews = {};
203
+ for (const item of libraryItems.slice(0, 40)) {
204
+ if (cancelled || !Array.isArray(item.elements) || item.elements.length === 0) {
205
+ continue;
206
+ }
207
+ try {
208
+ const canvas = await exportToCanvas({
209
+ elements: item.elements,
210
+ appState: {
211
+ exportBackground: true,
212
+ viewBackgroundColor: "#ffffff",
213
+ },
214
+ files: currentFilesForFile(),
215
+ exportPadding: 12,
216
+ maxWidthOrHeight: 320,
217
+ });
218
+ previews[item.id] = canvas.toDataURL("image/png");
219
+ } catch (error) {
220
+ console.warn("OpenPrd canvas library preview failed", error);
221
+ }
222
+ }
223
+ if (!cancelled) {
224
+ setLibraryPreviews(previews);
225
+ }
226
+ }
227
+ buildPreviews();
228
+ return () => {
229
+ cancelled = true;
230
+ };
231
+ }, [initialData, libraryItems]);
232
+
233
+ function changeLocale(event) {
234
+ setLocale(normalizeLocale(event.target.value));
235
+ }
236
+
237
+ async function copyThreadPrompt() {
238
+ const copied = await copyTextToClipboard(buildThreadPrompt(t));
239
+ setActionStatus({
240
+ type: copied ? "success" : "error",
241
+ message: copied ? t.threadPromptCopied : t.threadPromptCopyFailed,
242
+ });
243
+ }
244
+
245
+ function resetCanvas() {
246
+ if (!window.confirm(t.resetConfirm)) {
247
+ return;
248
+ }
249
+ latestElements = [];
250
+ latestFiles = {};
251
+ latestAppState = normalizeCanvasAppState({
252
+ ...latestAppState,
253
+ selectedElementIds: {},
254
+ });
255
+ excalidrawAPI?.updateScene?.({ elements: [], appState: latestAppState });
256
+ setSelectedCount(0);
257
+ setSelectedImageCount(0);
258
+ setActionStatus({ type: "success", message: t.resetCanvas });
259
+ queueSave();
260
+ }
261
+
262
+ async function persistLibraryItems(items, intent = "sync-excalidraw-library") {
263
+ return fetchJson("/api/library", {
264
+ method: "POST",
265
+ body: JSON.stringify({
266
+ type: "excalidrawlib",
267
+ version: 2,
268
+ source: "https://openprd.local/canvas",
269
+ libraryItems: items,
270
+ intent,
271
+ locale,
272
+ }),
273
+ });
274
+ }
275
+
276
+ function handleLibraryChange(items) {
277
+ const nextItems = Array.isArray(items) ? items : [];
278
+ setLibraryItems(nextItems);
279
+ persistLibraryItems(nextItems).catch((error) => {
280
+ console.warn("OpenPrd canvas library save failed", error);
281
+ });
282
+ }
283
+
284
+ async function openContainingFolder(target) {
285
+ const downloadFileName = target?.downloadFileName || "";
286
+ const targetPath = target?.openPath || "";
287
+ if ((!downloadFileName && !targetPath) || isOpeningFolder) {
288
+ return;
289
+ }
290
+ setIsOpeningFolder(true);
291
+ try {
292
+ await fetchJson(downloadFileName ? "/api/open-download" : "/api/open-folder", {
293
+ method: "POST",
294
+ body: JSON.stringify(downloadFileName ? { filename: downloadFileName } : { path: targetPath }),
295
+ });
296
+ } catch (error) {
297
+ console.warn("OpenPrd canvas open folder failed", error);
298
+ setActionStatus((current) => ({
299
+ type: "error",
300
+ message: t.openFolderFailed,
301
+ openPath: current?.openPath || targetPath,
302
+ downloadFileName: current?.downloadFileName || downloadFileName,
303
+ }));
304
+ } finally {
305
+ setIsOpeningFolder(false);
306
+ }
307
+ }
308
+
309
+ async function handleSaveCanvas() {
310
+ setActionStatus({ type: "busy", message: t.savingCanvas });
311
+ try {
312
+ const result = await saveCanvasToDisk();
313
+ setActionStatus({
314
+ type: "success",
315
+ message: formatMessage(t.savedCanvas, { filename: result.filename }),
316
+ downloadFileName: result.filename,
317
+ });
318
+ } catch (error) {
319
+ console.warn("OpenPrd canvas save-as failed", error);
320
+ setActionStatus({ type: "error", message: t.saveFailed });
321
+ }
322
+ }
323
+
324
+ async function handleExportImage() {
325
+ if (isExporting) {
326
+ return;
327
+ }
328
+ setIsExporting(true);
329
+ setActionStatus({ type: "busy", message: t.exportingImage });
330
+ try {
331
+ const exportData = await exportCanvasImage(locale);
332
+ const path = exportData.exportPath || exportData.fileName || exportData.exportUrl || "";
333
+ setActionStatus({
334
+ type: "success",
335
+ message: formatMessage(t.exportedImage, { path }),
336
+ openPath: exportData.exportPath || "",
337
+ });
338
+ } catch (error) {
339
+ console.warn("OpenPrd canvas image export failed", error);
340
+ setActionStatus({ type: "error", message: error?.code === "empty-scene" ? t.emptySceneExportBody : t.exportFailed });
341
+ } finally {
342
+ setIsExporting(false);
343
+ }
344
+ }
345
+
346
+ async function downloadSelectedImage() {
347
+ if (isDownloadingSelection || selectedImageCount <= 0) {
348
+ return;
349
+ }
350
+ setIsDownloadingSelection(true);
351
+ setActionStatus({ type: "busy", message: t.downloadingSelectedImage });
352
+ try {
353
+ const filename = createExportFilename("openprd-selected-image");
354
+ const exported = await exportCanvasElements({
355
+ requireSelection: true,
356
+ intent: "download-selected-image",
357
+ locale,
358
+ filename,
359
+ });
360
+ const exportData = exported.result.export || {};
361
+ const result = await fetchJson("/api/download-image", {
362
+ method: "POST",
363
+ body: JSON.stringify({
364
+ path: exportData.exportPath,
365
+ filename: exportData.fileName || filename,
366
+ }),
367
+ });
368
+ const fileName = result.file?.fileName || exportData.fileName || filename;
369
+ setActionStatus({
370
+ type: "success",
371
+ message: formatMessage(t.downloadedSelectedImage, { filename: fileName }),
372
+ downloadFileName: fileName,
373
+ });
374
+ } catch (error) {
375
+ console.warn("OpenPrd canvas selected image download failed", error);
376
+ setActionStatus({ type: "error", message: t.downloadSelectedImageFailed });
377
+ } finally {
378
+ setIsDownloadingSelection(false);
379
+ }
380
+ }
381
+
382
+ function libraryItemName(item, index) {
383
+ return item?.name || formatMessage(t.libraryItemName, {
384
+ index: index + 1,
385
+ count: Array.isArray(item?.elements) ? item.elements.length : 0,
386
+ });
387
+ }
388
+
389
+ function insertLibraryItem(item) {
390
+ if (!item || !Array.isArray(item.elements) || item.elements.length === 0) {
391
+ return;
392
+ }
393
+ const bounds = boundsForElements(item.elements);
394
+ const point = getViewportDropPoint(bounds.width, bounds.height);
395
+ const offsetX = point.x - bounds.x;
396
+ const offsetY = point.y - bounds.y;
397
+ const cloned = item.elements.map((element) => cloneLibraryElement(element, offsetX, offsetY));
398
+ updateScene([...latestElements, ...cloned]);
399
+ setActionStatus({ type: "success", message: t.insertedLibraryItem });
400
+ }
401
+
402
+ function updateComposerInstruction(event) {
403
+ const value = event.target.value;
404
+ setComposer((current) => current ? { ...current, instruction: value, status: current.status === "sent" ? "ready" : current.status, promptText: "" } : current);
405
+ }
406
+
407
+ async function prepareSelectionForCodex() {
408
+ if (isSending) {
409
+ return;
410
+ }
411
+ setIsSending(true);
412
+ setComposer(null);
413
+ try {
414
+ const selected = currentSelectedElements();
415
+ if (selected.length === 0) {
416
+ setComposer({
417
+ status: "error",
418
+ title: t.emptySelectionTitle,
419
+ body: t.emptySelectionBody,
420
+ });
421
+ return;
422
+ }
423
+ const filename = createExportFilename("codex-selection");
424
+ const exported = await exportCanvasElements({
425
+ requireSelection: true,
426
+ intent: "codex-handoff",
427
+ locale,
428
+ filename,
429
+ });
430
+ const exportPath = exported.result.export.exportPath;
431
+ setComposer({
432
+ status: "ready",
433
+ title: t.composerTitle,
434
+ body: "",
435
+ exported,
436
+ exportPath,
437
+ exportUrl: exported.result.export.exportUrl,
438
+ elementCount: exported.elements.length,
439
+ instruction: "",
440
+ promptText: "",
441
+ });
442
+ } catch (error) {
443
+ setComposer({
444
+ status: "error",
445
+ title: t.sendErrorTitle,
446
+ body: error.message || String(error),
447
+ });
448
+ } finally {
449
+ setIsSending(false);
450
+ }
451
+ }
452
+
453
+ async function sendComposerToCodex() {
454
+ if (!composer?.exported || isSending) {
455
+ return;
456
+ }
457
+ setIsSending(true);
458
+ setComposer((current) => current ? { ...current, status: "sending", error: "" } : current);
459
+ try {
460
+ const instruction = String(composer.instruction || "").trim();
461
+ const annotationContext = annotationEditContextFor(composer.exported.selected);
462
+ const promptText = buildCodexPrompt(t, composer.exportPath, composer.elementCount, instruction, annotationContext);
463
+ let relayTimedOut = false;
464
+ const relay = await withTimeout(recordCodexHandoff(composer.exported, promptText, locale, instruction, annotationContext), codexRelayUiTimeoutMs).catch((error) => {
465
+ relayTimedOut = error?.message === "timeout";
466
+ console.warn("OpenPrd canvas handoff record failed", error);
467
+ return null;
468
+ });
469
+ const copied = await withTimeout(copyTextToClipboard(promptText), 1800).catch(() => false);
470
+ const relayed = relayWasAccepted(relay);
471
+ const agentBridgePending = agentBridgeIsPending(relay);
472
+ const relayPending = relayIsPending(relay, relayTimedOut);
473
+ setComposer((current) => current ? {
474
+ ...current,
475
+ status: "sent",
476
+ copied,
477
+ body: agentBridgePending ? t.sentBodyAgentBridge : (relayed ? t.sentBodyWithRelay : (relayPending ? t.sentBodyPending : (copied ? t.sentBody : t.sendManualCopyBody))),
478
+ promptText,
479
+ } : current);
480
+ } catch (error) {
481
+ setComposer((current) => current ? {
482
+ ...current,
483
+ status: "ready",
484
+ error: error.message || String(error),
485
+ } : current);
486
+ } finally {
487
+ setIsSending(false);
488
+ }
489
+ }
490
+
491
+ async function copyCurrentPrompt() {
492
+ if (composer?.promptText) {
493
+ await copyTextToClipboard(composer.promptText);
494
+ }
495
+ }
496
+
497
+ function openImageTemplate(template) {
498
+ setImageComposer({
499
+ status: "ready",
500
+ template,
501
+ instruction: "",
502
+ promptText: "",
503
+ error: "",
504
+ });
505
+ }
506
+
507
+ function updateImageComposerInstruction(event) {
508
+ const value = event.target.value;
509
+ setImageComposer((current) => current ? {
510
+ ...current,
511
+ instruction: value,
512
+ status: current.status === "sent" ? "ready" : current.status,
513
+ error: "",
514
+ } : current);
515
+ }
516
+
517
+ async function submitImageTemplateRequest() {
518
+ if (!imageComposer?.template || isRequestingImage) {
519
+ return;
520
+ }
521
+ const instruction = String(imageComposer.instruction || "").trim();
522
+ if (!instruction) {
523
+ setImageComposer((current) => current ? { ...current, error: t.imagePromptRequired } : current);
524
+ return;
525
+ }
526
+ setIsRequestingImage(true);
527
+ setImageComposer((current) => current ? { ...current, status: "sending", error: "" } : current);
528
+ try {
529
+ const created = createAiImageCard(imageComposer.template, instruction, t);
530
+ const promptText = buildImageRequestPrompt(t, imageComposer.template, instruction, created.placeholderId);
531
+ attachAiImagePrompt(created.placeholderId, promptText);
532
+ let relayTimedOut = false;
533
+ const relay = await withTimeout(recordCodexImageRequestHandoff(promptText, locale, instruction, imageComposer.template, created), codexRelayUiTimeoutMs).catch((error) => {
534
+ relayTimedOut = error?.message === "timeout";
535
+ console.warn("OpenPrd canvas image handoff record failed", error);
536
+ return null;
537
+ });
538
+ const copied = await withTimeout(copyTextToClipboard(promptText), 1800).catch(() => false);
539
+ const relayed = relayWasAccepted(relay);
540
+ const agentBridgePending = agentBridgeIsPending(relay);
541
+ const relayPending = relayIsPending(relay, relayTimedOut);
542
+ setActionStatus({
543
+ type: relayed ? "success" : (agentBridgePending || relayPending ? "busy" : "success"),
544
+ message: agentBridgePending ? t.imageRequestAgentBridge : (relayed ? t.imageRequestSent : (relayPending ? t.imageRequestPending : (copied ? t.imageRequestPrepared : t.imageRequestManualCopy))),
545
+ });
546
+ setImageComposer(null);
547
+ } catch (error) {
548
+ console.warn("OpenPrd canvas image request failed", error);
549
+ setImageComposer((current) => current ? {
550
+ ...current,
551
+ status: "ready",
552
+ error: error.message || String(error),
553
+ } : current);
554
+ } finally {
555
+ setIsRequestingImage(false);
556
+ }
557
+ }
558
+
559
+ if (loadError) {
560
+ return h("div", { className: "fallback" },
561
+ h("div", null,
562
+ h("strong", null, t.loadErrorTitle),
563
+ h("span", null, loadError.message || String(loadError)),
564
+ ),
565
+ );
566
+ }
567
+
568
+ if (!initialData) {
569
+ return h("div", { className: "fallback" },
570
+ h("div", null,
571
+ h("strong", null, t.loadingTitle),
572
+ h("span", null, t.loadingBody),
573
+ ),
574
+ );
575
+ }
576
+
577
+ return h("div", { className: "shell" },
578
+ h("header", { className: "topbar" },
579
+ h("div", { className: "brand" },
580
+ h("strong", null, "OpenPrd Canvas"),
581
+ h("span", null, t.appSubtitle),
582
+ ),
583
+ h("div", { className: "actions" },
584
+ h("select", {
585
+ className: "language-select",
586
+ "aria-label": t.languageLabel,
587
+ "data-testid": "openprd-canvas-locale",
588
+ value: locale,
589
+ onChange: changeLocale,
590
+ },
591
+ h("option", { value: "zh-CN" }, "中文"),
592
+ h("option", { value: "en" }, "English"),
593
+ ),
594
+ ),
595
+ ),
596
+ h("main", { className: "layout" },
597
+ h("section", { className: "canvas-wrap" },
598
+ h(Excalidraw, {
599
+ excalidrawAPI: (api) => {
600
+ excalidrawAPI = api;
601
+ window.__openprdExcalidrawAPI = api;
602
+ },
603
+ initialData,
604
+ name: "OpenPrd Canvas",
605
+ langCode: locale,
606
+ UIOptions: {
607
+ canvasActions: {
608
+ loadScene: false,
609
+ saveToActiveFile: false,
610
+ export: {
611
+ saveFileToDisk: true,
612
+ },
613
+ },
614
+ },
615
+ onChange: (elements, appState, files) => {
616
+ latestElements = elements || [];
617
+ latestAppState = normalizeCanvasAppState(appState || {});
618
+ latestFiles = files || {};
619
+ setSelectedCount(selectedCountFromAppState(latestAppState));
620
+ setSelectedImageCount(selectedImageCountFromAppState(latestAppState, latestElements));
621
+ queueSave();
622
+ },
623
+ onLibraryChange: handleLibraryChange,
624
+ }),
625
+ selectedCount > 0 && h("div", { className: "selection-sendbar", "data-testid": "openprd-canvas-selection-sendbar" },
626
+ h("strong", null, formatMessage(t.selectedCount, { count: selectedCount })),
627
+ selectedImageCount > 0 && h("button", {
628
+ className: "text-button subtle-button",
629
+ disabled: isDownloadingSelection || isSending,
630
+ onClick: downloadSelectedImage,
631
+ "data-testid": "openprd-canvas-download-image",
632
+ }, toolIcon("export"), h("span", null, isDownloadingSelection ? t.downloadingSelectedImage : t.downloadSelectedImage)),
633
+ h("button", {
634
+ className: "text-button primary-button",
635
+ disabled: isSending,
636
+ onClick: prepareSelectionForCodex,
637
+ "data-testid": "openprd-canvas-send-codex",
638
+ }, toolIcon("send"), h("span", null, isSending ? t.sendingToCodex : t.sendToCodex)),
639
+ ),
640
+ composer && h("div", { className: "composer-backdrop", "data-testid": "openprd-canvas-composer" },
641
+ h("section", { className: "composer-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "openprd-canvas-composer-title" },
642
+ h("div", { className: "composer-header" },
643
+ h("h2", { id: "openprd-canvas-composer-title" }, composer.title || t.composerTitle),
644
+ h("button", { className: "text-button", onClick: () => setComposer(null) }, h("span", null, t.dismiss)),
645
+ ),
646
+ h("div", { className: "composer-body" },
647
+ composer.exportUrl && h(React.Fragment, null,
648
+ h("p", { className: "composer-label" }, t.previewLabel),
649
+ h("img", { className: "composer-preview", src: composer.exportUrl, alt: t.composerPreviewAlt }),
650
+ ),
651
+ composer.exported && h(React.Fragment, null,
652
+ h("p", { className: "composer-label" }, t.instructionLabel),
653
+ h("textarea", {
654
+ className: "composer-input",
655
+ value: composer.instruction || "",
656
+ placeholder: t.instructionPlaceholder,
657
+ disabled: isSending || composer.status === "sent",
658
+ onChange: updateComposerInstruction,
659
+ "data-testid": "openprd-canvas-composer-prompt",
660
+ }),
661
+ ),
662
+ composer.body && h("p", { className: "composer-status" }, composer.body),
663
+ composer.error && h("p", { className: "composer-error" }, composer.error),
664
+ ),
665
+ h("div", { className: "composer-actions" },
666
+ composer.promptText && h("button", { className: "text-button", onClick: copyCurrentPrompt }, toolIcon("copy"), h("span", null, t.copyPrompt)),
667
+ composer.exported && h("button", {
668
+ className: "text-button primary-button",
669
+ disabled: isSending || composer.status === "sent",
670
+ onClick: sendComposerToCodex,
671
+ "data-testid": "openprd-canvas-composer-submit",
672
+ }, toolIcon("send"), h("span", null, isSending || composer.status === "sending" ? t.sendingToCodex : t.sendToCodex)),
673
+ ),
674
+ ),
675
+ ),
676
+ imageComposer && h("div", { className: "composer-backdrop", "data-testid": "openprd-canvas-image-composer" },
677
+ h("section", { className: "composer-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "openprd-canvas-image-composer-title" },
678
+ h("div", { className: "composer-header" },
679
+ h("h2", { id: "openprd-canvas-image-composer-title" }, t.aiImageComposerTitle),
680
+ h("button", { className: "text-button", onClick: () => setImageComposer(null), disabled: isRequestingImage }, h("span", null, t.dismiss)),
681
+ ),
682
+ h("div", { className: "composer-body" },
683
+ h("p", { className: "composer-label" }, t.aiImageTemplateLabel),
684
+ h("div", { className: "template-compose-preview" },
685
+ h("div", {
686
+ className: "template-compose-frame",
687
+ style: { aspectRatio: imageComposer.template.width + " / " + imageComposer.template.height },
688
+ }, h("span", null, imageComposer.template.label)),
689
+ ),
690
+ h("p", { className: "composer-label" }, t.aiImageInstructionLabel),
691
+ h("textarea", {
692
+ className: "composer-input",
693
+ value: imageComposer.instruction || "",
694
+ placeholder: t.aiImageInstructionPlaceholder,
695
+ disabled: isRequestingImage,
696
+ onChange: updateImageComposerInstruction,
697
+ "data-testid": "openprd-canvas-image-prompt",
698
+ }),
699
+ imageComposer.error && h("p", { className: "composer-error" }, imageComposer.error),
700
+ ),
701
+ h("div", { className: "composer-actions" },
702
+ h("button", { className: "text-button", onClick: () => setImageComposer(null), disabled: isRequestingImage }, h("span", null, t.dismiss)),
703
+ h("button", {
704
+ className: "text-button primary-button",
705
+ disabled: isRequestingImage,
706
+ onClick: submitImageTemplateRequest,
707
+ "data-testid": "openprd-canvas-create-ai-image",
708
+ }, toolIcon("send"), h("span", null, isRequestingImage ? t.sendingToCodex : t.createAiImageCard)),
709
+ ),
710
+ ),
711
+ ),
712
+ ),
713
+ h("aside", { className: "rail" },
714
+ h("section", { className: "thread-summary", "data-testid": "openprd-canvas-thread-summary" },
715
+ h("p", { className: "thread-title" }, conversationTitle(t)),
716
+ h("div", { className: "thread-id-row" },
717
+ h("p", { className: "thread-id" }, conversationId()),
718
+ h("button", {
719
+ className: "thread-copy-button",
720
+ type: "button",
721
+ title: t.copyThreadPrompt,
722
+ "aria-label": t.copyThreadPrompt,
723
+ onClick: copyThreadPrompt,
724
+ "data-testid": "openprd-canvas-copy-thread",
725
+ }, toolIcon("copy")),
726
+ ),
727
+ ),
728
+ h("section", { className: "rail-actions", "data-testid": "openprd-canvas-rail-actions" },
729
+ h("div", { className: "rail-action-group", "data-testid": "openprd-canvas-review-board" },
730
+ h("p", { className: "rail-heading" }, t.reviewBoardTitle),
731
+ guide && h("div", { className: "guide-strip", "data-testid": "openprd-canvas-guide" },
732
+ h("strong", null, formatMessage(t.reviewGuideStage, {
733
+ index: guide.currentStage.index,
734
+ total: guide.totalStages,
735
+ title: guide.currentStage.title,
736
+ })),
737
+ guide.nextAction && h("span", null, formatMessage(t.reviewGuideNext, { action: guide.nextAction })),
738
+ ),
739
+ h("button", {
740
+ className: "rail-action-button",
741
+ title: t.importReviewDirections,
742
+ disabled: isImportingDirections,
743
+ onClick: handleImportReviewDirections,
744
+ "data-testid": "openprd-canvas-import-directions",
745
+ }, toolIcon("export"), h("span", null, isImportingDirections ? t.importingReviewDirections : t.importReviewDirections)),
746
+ ),
747
+ h("div", { className: "rail-action-group" },
748
+ h("p", { className: "rail-heading" }, t.canvasActionsTitle),
749
+ h("div", { className: "rail-action-grid" },
750
+ h("button", {
751
+ className: "rail-action-button",
752
+ title: t.saveCanvas,
753
+ onClick: handleSaveCanvas,
754
+ "data-testid": "openprd-canvas-save",
755
+ }, toolIcon("save"), h("span", null, t.saveCanvas)),
756
+ h("button", {
757
+ className: "rail-action-button",
758
+ title: t.exportImage,
759
+ disabled: isExporting,
760
+ onClick: handleExportImage,
761
+ "data-testid": "openprd-canvas-export-image",
762
+ }, toolIcon("export"), h("span", null, isExporting ? t.exportingImage : t.exportImage)),
763
+ h("button", {
764
+ className: "rail-action-button danger",
765
+ title: t.resetCanvas,
766
+ onClick: resetCanvas,
767
+ "data-testid": "openprd-canvas-reset",
768
+ }, toolIcon("reset"), h("span", null, t.resetCanvas)),
769
+ ),
770
+ actionStatus && h("div", {
771
+ className: "action-status" + (actionStatus.type === "error" ? " error" : "") + (actionStatus.type === "busy" ? " busy" : ""),
772
+ "data-testid": "openprd-canvas-action-status",
773
+ },
774
+ h("span", null, actionStatus.message),
775
+ (actionStatus.openPath || actionStatus.downloadFileName) && h("button", {
776
+ className: "status-folder-button",
777
+ disabled: isOpeningFolder,
778
+ onClick: () => openContainingFolder(actionStatus),
779
+ "data-testid": "openprd-canvas-open-folder",
780
+ }, isOpeningFolder ? t.openingFolder : t.openContainingFolder),
781
+ ),
782
+ ),
783
+ h("div", { className: "rail-action-group" },
784
+ h("p", { className: "rail-heading" }, t.aiImageTemplatesTitle),
785
+ h("div", { className: "template-grid", "data-testid": "openprd-canvas-ai-templates" },
786
+ aiTemplates.map((template) => h("button", {
787
+ key: template.id,
788
+ className: "template-tile",
789
+ title: template.title + " " + template.label,
790
+ onClick: () => openImageTemplate(template),
791
+ "data-testid": "openprd-canvas-ai-template-" + template.id,
792
+ },
793
+ h("span", {
794
+ className: "template-preview",
795
+ style: { aspectRatio: template.width + " / " + template.height },
796
+ "aria-hidden": "true",
797
+ }),
798
+ h("strong", null, template.label),
799
+ h("span", null, template.title),
800
+ )),
801
+ ),
802
+ ),
803
+ h("div", { className: "rail-action-group" },
804
+ h("p", { className: "rail-heading" }, t.personalLibraryTitle),
805
+ h("div", { className: "library-grid", "data-testid": "openprd-canvas-personal-library" },
806
+ libraryItems.map((item, index) => h("button", {
807
+ key: item.id || index,
808
+ className: "library-tile",
809
+ title: libraryItemName(item, index),
810
+ onClick: () => insertLibraryItem(item),
811
+ "data-testid": "openprd-canvas-library-item",
812
+ },
813
+ h("span", { className: "library-item-preview", "aria-hidden": "true" },
814
+ libraryPreviews[item.id]
815
+ ? h("img", { src: libraryPreviews[item.id], alt: "" })
816
+ : h("span", null, formatMessage(t.libraryItemCount, { count: Array.isArray(item.elements) ? item.elements.length : 0 })),
817
+ ),
818
+ h("span", null, libraryItemName(item, index)),
819
+ )),
820
+ h("button", {
821
+ className: "library-tile",
822
+ title: t.blankCard,
823
+ onClick: () => addBlankCard(t.placeholderText),
824
+ "data-testid": "openprd-canvas-blank-card",
825
+ },
826
+ h("span", { className: "library-card-preview", "aria-hidden": "true" }),
827
+ h("span", null, t.blankCard),
828
+ ),
829
+ ),
830
+ ),
831
+ ),
832
+ ),
833
+ ),
834
+ );
835
+ }
836
+
837
+ createRoot(document.getElementById("root")).render(h(CanvasApp));
838
+ `;