@tea-agent/loop-agent 0.26.0 → 0.26.1

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 (170) hide show
  1. package/CHANGELOG.md +1032 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/cli/command-definitions.js +25 -10
  5. package/dist/cli/help.js +4 -3
  6. package/dist/cli/program.js +43 -17
  7. package/dist/commands/cursor-prompt.js +6 -6
  8. package/dist/commands/import-prd.js +7 -2
  9. package/dist/commands/init.js +7 -5
  10. package/dist/commands/loop-benchmark.js +11 -11
  11. package/dist/commands/pi-reuse-benchmark.js +16 -16
  12. package/dist/commands/task-source-prepare.js +468 -0
  13. package/dist/executors/dag-pi-executor.js +40 -5
  14. package/dist/executors/shell-write-guard.js +161 -25
  15. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  16. package/dist/task/source-prepare/build-draft.js +215 -0
  17. package/dist/task/source-prepare/completeness.js +195 -0
  18. package/dist/task/source-prepare/index.js +7 -0
  19. package/dist/task/source-prepare/parse-intent.js +373 -0
  20. package/dist/task/source-prepare/path-policy.js +197 -0
  21. package/dist/task/source-prepare/prepare.js +506 -0
  22. package/dist/task/source-prepare/reference-integrity.js +274 -0
  23. package/dist/task/source-prepare/types.js +7 -0
  24. package/dist/worker/observe/static/copy.js +67 -67
  25. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  26. package/dist/worker/observe/static/dag-layout.js +83 -83
  27. package/dist/worker/observe/static/dom.js +220 -220
  28. package/dist/worker/observe/static/relations.js +133 -133
  29. package/dist/worker/observe/static/router.js +93 -93
  30. package/dist/worker/observe/static/run-processing.js +148 -148
  31. package/dist/worker/observe/static/views/batch.js +227 -227
  32. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  33. package/dist/worker/observe/static/views/failures.js +143 -143
  34. package/dist/worker/observe/static/views/feature.js +492 -492
  35. package/dist/worker/observe/static/views/run.js +453 -453
  36. package/dist/worker/observe/static/views/shell.js +7 -7
  37. package/dist/worker/observe/static/views/timeline.js +163 -163
  38. package/dist/workflows/dag/canvas-observer.js +275 -275
  39. package/docs/skills/README.md +7 -7
  40. package/docs/templates/adr.md +60 -60
  41. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  42. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  43. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  44. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  45. package/docs/templates/agent-dag-report.schema.json +473 -473
  46. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  47. package/docs/templates/backend-test-result.schema.json +99 -99
  48. package/docs/templates/feature-spec.md +53 -53
  49. package/docs/templates/frontend-design-contract.md +42 -42
  50. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  51. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  52. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  53. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  54. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  55. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  56. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  57. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  58. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  59. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  60. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  61. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  62. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  63. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  64. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  65. package/docs/templates/frontend-eval/metrics.md +138 -138
  66. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  67. package/docs/templates/frontend-task-constraints.md +35 -35
  68. package/docs/templates/frontend-task-requirement.md +70 -70
  69. package/docs/templates/init-evolution-review.md +35 -35
  70. package/docs/templates/init-managed-agents.md +10 -5
  71. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  72. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  73. package/docs/templates/knowledge-sync-dag.json +178 -178
  74. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  75. package/docs/templates/product-line/closeout.yaml +9 -9
  76. package/docs/templates/product-line/design.md +13 -13
  77. package/docs/templates/product-line/links.md +10 -10
  78. package/docs/templates/product-line/requirement.md +17 -17
  79. package/docs/templates/product-line/test-plan.md +7 -7
  80. package/docs/templates/production-readiness-checklist.md +57 -57
  81. package/docs/templates/project-start-checklist.md +9 -9
  82. package/docs/templates/qa-report.md +48 -48
  83. package/docs/templates/sprint-contract.md +29 -29
  84. package/docs/templates/worker-dogfood-evidence.md +80 -80
  85. package/docs/templates/worker-dogfood-setup.md +68 -68
  86. package/package.json +1 -1
  87. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  88. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  89. package/scripts/kb-graph-materialize.mjs +105 -105
  90. package/scripts/kb-graph-promote.mjs +164 -164
  91. package/scripts/kb-query.mjs +554 -554
  92. package/skills/ai-engineering-context/SKILL.md +48 -48
  93. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  94. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  95. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  96. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  97. package/skills/analyze-product-dependencies/references/example.md +76 -76
  98. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  99. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  100. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  101. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  102. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  103. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  104. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  105. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  106. package/skills/analyze-product-requirements/SKILL.md +90 -90
  107. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  108. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  109. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  110. package/skills/analyze-product-requirements/references/example.md +86 -86
  111. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  112. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  113. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  114. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  115. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  116. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  117. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  118. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  119. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  120. package/skills/browser-tools/browser-content.js +103 -103
  121. package/skills/browser-tools/browser-cookies.js +35 -35
  122. package/skills/browser-tools/browser-eval.js +53 -53
  123. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  124. package/skills/browser-tools/browser-nav.js +44 -44
  125. package/skills/browser-tools/browser-pick.js +162 -162
  126. package/skills/browser-tools/browser-screenshot.js +34 -34
  127. package/skills/browser-tools/browser-start.js +86 -86
  128. package/skills/browser-tools/package-lock.json +2556 -2556
  129. package/skills/browser-tools/package.json +19 -19
  130. package/skills/code-review-core/SKILL.md +20 -20
  131. package/skills/codebase-scout/SKILL.md +19 -19
  132. package/skills/grill-me/SKILL.md +10 -10
  133. package/skills/loop-agent/SKILL.md +5 -2
  134. package/skills/loop-agent/references/README.md +67 -67
  135. package/skills/loop-agent/references/command-reference.md +17 -15
  136. package/skills/loop-agent/references/docs-converge.md +126 -126
  137. package/skills/loop-agent/references/harness-policy.md +3 -4
  138. package/skills/loop-agent/references/learned/README.md +21 -21
  139. package/skills/loop-agent/references/long-running-loop.md +57 -57
  140. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  141. package/skills/loop-agent/references/pi-prompt.md +23 -23
  142. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  143. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  144. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  145. package/skills/loop-agent/references/task-workflow.md +7 -5
  146. package/skills/playwright-cli/SKILL.md +420 -420
  147. package/skills/playwright-cli/references/element-attributes.md +23 -23
  148. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  149. package/skills/playwright-cli/references/request-mocking.md +87 -87
  150. package/skills/playwright-cli/references/running-code.md +241 -241
  151. package/skills/playwright-cli/references/session-management.md +225 -225
  152. package/skills/playwright-cli/references/storage-state.md +275 -275
  153. package/skills/playwright-cli/references/test-generation.md +433 -433
  154. package/skills/playwright-cli/references/tracing.md +139 -139
  155. package/skills/playwright-cli/references/video-recording.md +143 -143
  156. package/skills/requesting-code-review/SKILL.md +101 -101
  157. package/skills/requesting-code-review/code-reviewer.md +168 -168
  158. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  159. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  160. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  161. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  162. package/skills/systematic-debugging/find-polluter.sh +63 -63
  163. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  164. package/skills/systematic-debugging/test-academic.md +14 -14
  165. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  166. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  167. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  168. package/skills/using-git-worktrees/SKILL.md +215 -215
  169. package/skills/verification-before-completion/SKILL.md +154 -154
  170. package/skills/webapp-testing/SKILL.md +19 -19
@@ -0,0 +1,274 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, open, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getTaskPaths } from "../runtime.js";
5
+ import { readSourceManifest, } from "../source-references.js";
6
+ import { PREPARE_MAX_FILE_BYTES, PREPARE_MAX_PARSE_DOCS, PREPARE_MAX_TOTAL_BYTES, } from "./types.js";
7
+ const PARSEABLE_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
8
+ const PARSEABLE_ROLES = new Set(["requirement", "acceptance"]);
9
+ function isSafeRelativeReferencePath(materializedPath) {
10
+ const normalized = materializedPath.replace(/\\/g, "/").trim();
11
+ if (!normalized)
12
+ return false;
13
+ if (normalized.includes("\0"))
14
+ return false;
15
+ if (path.isAbsolute(normalized) || /^[a-zA-Z]:[\\/]/.test(normalized)) {
16
+ return false;
17
+ }
18
+ if (normalized.split("/").some((part) => part === ".."))
19
+ return false;
20
+ if (!normalized.startsWith("references/"))
21
+ return false;
22
+ return true;
23
+ }
24
+ async function streamSha256AndBytes(filePath, maxBytes) {
25
+ const handle = await open(filePath, "r");
26
+ try {
27
+ const hash = createHash("sha256");
28
+ const buffer = Buffer.alloc(64 * 1024);
29
+ let bytes = 0;
30
+ while (true) {
31
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
32
+ if (bytesRead === 0)
33
+ break;
34
+ bytes += bytesRead;
35
+ if (maxBytes !== undefined && bytes > maxBytes) {
36
+ throw Object.assign(new Error("INPUT_TOO_LARGE"), {
37
+ code: "INPUT_TOO_LARGE",
38
+ bytes,
39
+ });
40
+ }
41
+ hash.update(buffer.subarray(0, bytesRead));
42
+ }
43
+ return { sha256: hash.digest("hex"), bytes };
44
+ }
45
+ finally {
46
+ await handle.close();
47
+ }
48
+ }
49
+ function isParseableDoc(doc) {
50
+ const ext = path.extname(doc.materializedPath).toLowerCase();
51
+ if (!PARSEABLE_EXTENSIONS.has(ext))
52
+ return false;
53
+ return PARSEABLE_ROLES.has(doc.role);
54
+ }
55
+ /**
56
+ * Validate source-manifest + reference files before deriving a draft.
57
+ * Fail-closed on taskId/path/hash/symlink/size issues.
58
+ */
59
+ export async function validateImportedPrdReferences(input) {
60
+ const paths = getTaskPaths(input.repoRoot, input.taskId);
61
+ const issues = [];
62
+ const documents = [];
63
+ const manifest = await readSourceManifest(paths.sourceDir);
64
+ if (!manifest) {
65
+ issues.push({
66
+ code: "MANIFEST_MISSING",
67
+ level: "blocking",
68
+ message: "source-manifest.json missing; run import-prd before --use-imported-prd",
69
+ path: path.join(paths.sourceDir, "source-manifest.json"),
70
+ });
71
+ return { ok: false, issues, documents, parseableDocuments: [] };
72
+ }
73
+ if (manifest.schemaVersion !== 1) {
74
+ issues.push({
75
+ code: "MANIFEST_SCHEMA",
76
+ level: "blocking",
77
+ message: `unsupported source-manifest schemaVersion: ${String(manifest.schemaVersion)}`,
78
+ });
79
+ }
80
+ if (manifest.taskId !== input.taskId) {
81
+ issues.push({
82
+ code: "MANIFEST_TASK_MISMATCH",
83
+ level: "blocking",
84
+ message: `manifest.taskId "${manifest.taskId}" does not match task ${input.taskId}`,
85
+ });
86
+ }
87
+ if (!Array.isArray(manifest.documents)) {
88
+ issues.push({
89
+ code: "MANIFEST_INVALID",
90
+ level: "blocking",
91
+ message: "source-manifest.documents must be an array",
92
+ });
93
+ return { ok: false, issues, documents, parseableDocuments: [] };
94
+ }
95
+ // Stable order by materializedPath (import already sorts; re-sort for safety).
96
+ const ordered = [...manifest.documents].sort((a, b) => a.materializedPath < b.materializedPath
97
+ ? -1
98
+ : a.materializedPath > b.materializedPath
99
+ ? 1
100
+ : 0);
101
+ let totalParseBytes = 0;
102
+ let parseableCount = 0;
103
+ for (const doc of ordered) {
104
+ if (!doc ||
105
+ typeof doc.role !== "string" ||
106
+ typeof doc.name !== "string" ||
107
+ typeof doc.materializedPath !== "string" ||
108
+ typeof doc.sha256 !== "string" ||
109
+ typeof doc.bytes !== "number") {
110
+ issues.push({
111
+ code: "MANIFEST_DOC_INVALID",
112
+ level: "blocking",
113
+ message: "manifest document missing required fields",
114
+ });
115
+ continue;
116
+ }
117
+ if (!isSafeRelativeReferencePath(doc.materializedPath)) {
118
+ issues.push({
119
+ code: "REFERENCE_PATH_UNSAFE",
120
+ level: "blocking",
121
+ message: `reference path not contained under references/: ${doc.materializedPath}`,
122
+ path: doc.materializedPath,
123
+ });
124
+ continue;
125
+ }
126
+ const absolutePath = path.resolve(paths.sourceDir, doc.materializedPath);
127
+ const relativeToSource = path
128
+ .relative(paths.sourceDir, absolutePath)
129
+ .split(path.sep)
130
+ .join("/");
131
+ if (relativeToSource.startsWith("..") ||
132
+ path.isAbsolute(relativeToSource)) {
133
+ issues.push({
134
+ code: "REFERENCE_PATH_ESCAPE",
135
+ level: "blocking",
136
+ message: `reference path escapes source dir: ${doc.materializedPath}`,
137
+ path: doc.materializedPath,
138
+ });
139
+ continue;
140
+ }
141
+ let stats;
142
+ try {
143
+ stats = await lstat(absolutePath);
144
+ }
145
+ catch {
146
+ issues.push({
147
+ code: "REFERENCE_MISSING",
148
+ level: "blocking",
149
+ message: `reference file missing: ${doc.materializedPath}`,
150
+ path: doc.materializedPath,
151
+ });
152
+ continue;
153
+ }
154
+ if (stats.isSymbolicLink()) {
155
+ issues.push({
156
+ code: "REFERENCE_SYMLINK",
157
+ level: "blocking",
158
+ message: `reference must be a regular file (symlink rejected): ${doc.materializedPath}`,
159
+ path: doc.materializedPath,
160
+ });
161
+ continue;
162
+ }
163
+ if (!stats.isFile()) {
164
+ issues.push({
165
+ code: "REFERENCE_NOT_FILE",
166
+ level: "blocking",
167
+ message: `reference is not a regular file: ${doc.materializedPath}`,
168
+ path: doc.materializedPath,
169
+ });
170
+ continue;
171
+ }
172
+ const parseable = isParseableDoc(doc);
173
+ if (parseable) {
174
+ parseableCount += 1;
175
+ if (parseableCount > PREPARE_MAX_PARSE_DOCS) {
176
+ issues.push({
177
+ code: "INPUT_TOO_LARGE",
178
+ level: "blocking",
179
+ message: `more than ${PREPARE_MAX_PARSE_DOCS} parseable imported docs`,
180
+ });
181
+ continue;
182
+ }
183
+ }
184
+ let hashResult;
185
+ try {
186
+ hashResult = await streamSha256AndBytes(absolutePath, parseable ? PREPARE_MAX_FILE_BYTES : undefined);
187
+ }
188
+ catch (error) {
189
+ if (error &&
190
+ typeof error === "object" &&
191
+ "code" in error &&
192
+ error.code === "INPUT_TOO_LARGE") {
193
+ issues.push({
194
+ code: "INPUT_TOO_LARGE",
195
+ level: "blocking",
196
+ message: `reference exceeds ${PREPARE_MAX_FILE_BYTES} bytes: ${doc.materializedPath}`,
197
+ path: doc.materializedPath,
198
+ });
199
+ continue;
200
+ }
201
+ issues.push({
202
+ code: "REFERENCE_READ_FAILED",
203
+ level: "blocking",
204
+ message: `failed to hash reference: ${doc.materializedPath}`,
205
+ path: doc.materializedPath,
206
+ });
207
+ continue;
208
+ }
209
+ if (hashResult.bytes !== doc.bytes) {
210
+ issues.push({
211
+ code: "REFERENCE_BYTES_MISMATCH",
212
+ level: "blocking",
213
+ message: `bytes mismatch for ${doc.materializedPath}: manifest=${doc.bytes} disk=${hashResult.bytes}`,
214
+ path: doc.materializedPath,
215
+ });
216
+ }
217
+ if (hashResult.sha256 !== doc.sha256) {
218
+ issues.push({
219
+ code: "REFERENCE_HASH_MISMATCH",
220
+ level: "blocking",
221
+ message: `sha256 mismatch for ${doc.materializedPath}`,
222
+ path: doc.materializedPath,
223
+ });
224
+ }
225
+ let content;
226
+ if (parseable) {
227
+ totalParseBytes += hashResult.bytes;
228
+ if (totalParseBytes > PREPARE_MAX_TOTAL_BYTES) {
229
+ issues.push({
230
+ code: "INPUT_TOO_LARGE",
231
+ level: "blocking",
232
+ message: `total parseable reference bytes exceed ${PREPARE_MAX_TOTAL_BYTES}`,
233
+ });
234
+ }
235
+ else {
236
+ content = await readFile(absolutePath, "utf-8");
237
+ }
238
+ }
239
+ documents.push({
240
+ role: doc.role,
241
+ name: doc.name,
242
+ materializedPath: doc.materializedPath,
243
+ absolutePath,
244
+ sha256: hashResult.sha256,
245
+ bytes: hashResult.bytes,
246
+ content,
247
+ parseable,
248
+ });
249
+ }
250
+ const parseableDocuments = documents.filter((doc) => doc.parseable);
251
+ const hasRequirement = parseableDocuments.some((doc) => doc.role === "requirement");
252
+ if (input.requireParseableRequirement !== false && !hasRequirement) {
253
+ // Still allow if caller will supply supplemental flags/text; prepare.ts decides.
254
+ // Here we only mark warning-level absence; prepare elevates when needed.
255
+ issues.push({
256
+ code: "NO_PARSEABLE_REQUIREMENT",
257
+ level: "warning",
258
+ message: "no parseable requirement-role document under source/references (role=requirement, .md/.txt)",
259
+ });
260
+ }
261
+ const blocking = issues.some((issue) => issue.level === "blocking");
262
+ return {
263
+ ok: !blocking,
264
+ issues,
265
+ documents,
266
+ parseableDocuments,
267
+ };
268
+ }
269
+ export function draftReferencesFromManifest(documents) {
270
+ return documents.map((doc) => ({
271
+ role: doc.role,
272
+ ref: doc.materializedPath,
273
+ }));
274
+ }
@@ -0,0 +1,7 @@
1
+ export const PREPARE_MAX_FILE_BYTES = 2 * 1024 * 1024;
2
+ export const PREPARE_MAX_PARSE_DOCS = 8;
3
+ export const PREPARE_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
4
+ export const PREPARE_DEFAULT_FORBIDDEN_PATHS = [
5
+ ".harness/**",
6
+ "node_modules/**",
7
+ ];
@@ -1,67 +1,67 @@
1
- /** Copy-only recommended commands (Observe UI R5). */
2
- import { el } from "./dom.js";
3
-
4
- export async function copyText(text) {
5
- const value = String(text ?? "");
6
- if (!value) return false;
7
- try {
8
- if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
9
- await navigator.clipboard.writeText(value);
10
- return true;
11
- }
12
- } catch {
13
- // fall through
14
- }
15
- try {
16
- const ta = document.createElement("textarea");
17
- ta.value = value;
18
- ta.setAttribute("readonly", "");
19
- ta.style.position = "fixed";
20
- ta.style.left = "-9999px";
21
- document.body.appendChild(ta);
22
- ta.select();
23
- const ok = document.execCommand("copy");
24
- document.body.removeChild(ta);
25
- return ok;
26
- } catch {
27
- return false;
28
- }
29
- }
30
-
31
- /** Advisory recommended command: copy-only, never executes. */
32
- export function renderRecommendedCommand(action) {
33
- const block = el("div", "recommended-command");
34
- if (!action) {
35
- block.appendChild(el("span", "muted", "缺失"));
36
- return block;
37
- }
38
- const head = el("div", "recommended-command-head");
39
- if (action.kind) {
40
- head.appendChild(el("span", "recommended-command-kind", action.kind));
41
- }
42
- head.appendChild(
43
- el("span", "recommended-command-label", action.label ?? "—"),
44
- );
45
- block.appendChild(head);
46
- if (action.command) {
47
- const body = el("div", "recommended-command-body");
48
- const code = el("code", "recommended-command-text", action.command);
49
- body.appendChild(code);
50
- const btn = el("button", "copy-command", "复制");
51
- btn.type = "button";
52
- btn.addEventListener("click", (e) => {
53
- e.preventDefault();
54
- e.stopPropagation();
55
- void copyText(action.command).then((ok) => {
56
- btn.textContent = ok ? "已复制" : "复制失败";
57
- setTimeout(() => {
58
- btn.textContent = "复制";
59
- }, 1500);
60
- });
61
- });
62
- body.appendChild(btn);
63
- block.appendChild(body);
64
- }
65
- return block;
66
- }
67
-
1
+ /** Copy-only recommended commands (Observe UI R5). */
2
+ import { el } from "./dom.js";
3
+
4
+ export async function copyText(text) {
5
+ const value = String(text ?? "");
6
+ if (!value) return false;
7
+ try {
8
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
9
+ await navigator.clipboard.writeText(value);
10
+ return true;
11
+ }
12
+ } catch {
13
+ // fall through
14
+ }
15
+ try {
16
+ const ta = document.createElement("textarea");
17
+ ta.value = value;
18
+ ta.setAttribute("readonly", "");
19
+ ta.style.position = "fixed";
20
+ ta.style.left = "-9999px";
21
+ document.body.appendChild(ta);
22
+ ta.select();
23
+ const ok = document.execCommand("copy");
24
+ document.body.removeChild(ta);
25
+ return ok;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /** Advisory recommended command: copy-only, never executes. */
32
+ export function renderRecommendedCommand(action) {
33
+ const block = el("div", "recommended-command");
34
+ if (!action) {
35
+ block.appendChild(el("span", "muted", "缺失"));
36
+ return block;
37
+ }
38
+ const head = el("div", "recommended-command-head");
39
+ if (action.kind) {
40
+ head.appendChild(el("span", "recommended-command-kind", action.kind));
41
+ }
42
+ head.appendChild(
43
+ el("span", "recommended-command-label", action.label ?? "—"),
44
+ );
45
+ block.appendChild(head);
46
+ if (action.command) {
47
+ const body = el("div", "recommended-command-body");
48
+ const code = el("code", "recommended-command-text", action.command);
49
+ body.appendChild(code);
50
+ const btn = el("button", "copy-command", "复制");
51
+ btn.type = "button";
52
+ btn.addEventListener("click", (e) => {
53
+ e.preventDefault();
54
+ e.stopPropagation();
55
+ void copyText(action.command).then((ok) => {
56
+ btn.textContent = ok ? "已复制" : "复制失败";
57
+ setTimeout(() => {
58
+ btn.textContent = "复制";
59
+ }, 1500);
60
+ });
61
+ });
62
+ body.appendChild(btn);
63
+ block.appendChild(body);
64
+ }
65
+ return block;
66
+ }
67
+
@@ -1,31 +1,31 @@
1
- export type DagLayoutNodeInput = { nodeId: string };
2
- export type DagLayoutEdgeInput = { from: string; to: string };
3
- export type DagLayoutNode = {
4
- nodeId: string;
5
- rank: number;
6
- index: number;
7
- x: number;
8
- y: number;
9
- width: number;
10
- height: number;
11
- };
12
- export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
13
- export type DagLayout = {
14
- width: number;
15
- height: number;
16
- nodes: DagLayoutNode[];
17
- edges: DagLayoutEdge[];
18
- };
19
-
20
- export function layoutDag(
21
- nodes?: DagLayoutNodeInput[],
22
- edges?: DagLayoutEdgeInput[],
23
- ranks?: string[][],
24
- options?: Partial<{
25
- nodeWidth: number;
26
- nodeHeight: number;
27
- rankGap: number;
28
- nodeGap: number;
29
- padding: number;
30
- }>,
31
- ): DagLayout;
1
+ export type DagLayoutNodeInput = { nodeId: string };
2
+ export type DagLayoutEdgeInput = { from: string; to: string };
3
+ export type DagLayoutNode = {
4
+ nodeId: string;
5
+ rank: number;
6
+ index: number;
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ };
12
+ export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
13
+ export type DagLayout = {
14
+ width: number;
15
+ height: number;
16
+ nodes: DagLayoutNode[];
17
+ edges: DagLayoutEdge[];
18
+ };
19
+
20
+ export function layoutDag(
21
+ nodes?: DagLayoutNodeInput[],
22
+ edges?: DagLayoutEdgeInput[],
23
+ ranks?: string[][],
24
+ options?: Partial<{
25
+ nodeWidth: number;
26
+ nodeHeight: number;
27
+ rankGap: number;
28
+ nodeGap: number;
29
+ padding: number;
30
+ }>,
31
+ ): DagLayout;
@@ -1,83 +1,83 @@
1
- const DEFAULTS = {
2
- nodeWidth: 216,
3
- nodeHeight: 92,
4
- rankGap: 104,
5
- nodeGap: 28,
6
- padding: 32,
7
- };
8
-
9
- export function layoutDag(nodes = [], edges = [], ranks = [], options = {}) {
10
- const config = { ...DEFAULTS, ...options };
11
- const nodeIds = [...new Set(nodes.map((node) => node.nodeId).filter(Boolean))].sort();
12
- if (nodeIds.length === 0) return { width: 320, height: 160, nodes: [], edges: [] };
13
- const known = new Set(nodeIds);
14
- const validEdges = edges
15
- .filter((edge) => known.has(edge.from) && known.has(edge.to) && edge.from !== edge.to)
16
- .sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
17
- const layers = normalizeLayers(nodeIds, validEdges, ranks);
18
- const positioned = [];
19
- for (let rank = 0; rank < layers.length; rank++) {
20
- const ids = layers[rank];
21
- for (let index = 0; index < ids.length; index++) {
22
- positioned.push({
23
- nodeId: ids[index], rank, index,
24
- x: config.padding + rank * (config.nodeWidth + config.rankGap),
25
- y: config.padding + index * (config.nodeHeight + config.nodeGap),
26
- width: config.nodeWidth, height: config.nodeHeight,
27
- });
28
- }
29
- }
30
- const byId = new Map(positioned.map((node) => [node.nodeId, node]));
31
- const routed = validEdges.map((edge) => {
32
- const from = byId.get(edge.from);
33
- const to = byId.get(edge.to);
34
- const x1 = from.x + from.width;
35
- const y1 = from.y + from.height / 2;
36
- const x2 = to.x;
37
- const y2 = to.y + to.height / 2;
38
- const bend = Math.max(36, (x2 - x1) / 2);
39
- return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
40
- });
41
- const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
42
- return {
43
- width: config.padding * 2 + layers.length * config.nodeWidth + (layers.length - 1) * config.rankGap,
44
- height: config.padding * 2 + maxRows * config.nodeHeight + (maxRows - 1) * config.nodeGap,
45
- nodes: positioned,
46
- edges: routed,
47
- };
48
- }
49
-
50
- function normalizeLayers(nodeIds, edges, ranks) {
51
- const known = new Set(nodeIds);
52
- const seen = new Set();
53
- const layers = [];
54
- if (Array.isArray(ranks)) {
55
- for (const rank of ranks) {
56
- if (!Array.isArray(rank)) continue;
57
- const ids = [...new Set(rank.filter((id) => known.has(id) && !seen.has(id)))].sort();
58
- ids.forEach((id) => seen.add(id));
59
- if (ids.length > 0) layers.push(ids);
60
- }
61
- }
62
- if (layers.length === 0 && edges.length > 0) {
63
- const level = new Map(nodeIds.map((id) => [id, 0]));
64
- for (let pass = 0; pass < nodeIds.length; pass++) {
65
- let changed = false;
66
- for (const edge of edges) {
67
- const next = Math.min(nodeIds.length - 1, level.get(edge.from) + 1);
68
- if (next > level.get(edge.to)) { level.set(edge.to, next); changed = true; }
69
- }
70
- if (!changed) break;
71
- }
72
- for (const id of nodeIds) {
73
- const rank = level.get(id);
74
- (layers[rank] ??= []).push(id);
75
- seen.add(id);
76
- }
77
- for (const layer of layers) layer?.sort();
78
- }
79
- for (const id of nodeIds) {
80
- if (!seen.has(id)) (layers[0] ??= []).push(id);
81
- }
82
- return layers.filter(Boolean);
83
- }
1
+ const DEFAULTS = {
2
+ nodeWidth: 216,
3
+ nodeHeight: 92,
4
+ rankGap: 104,
5
+ nodeGap: 28,
6
+ padding: 32,
7
+ };
8
+
9
+ export function layoutDag(nodes = [], edges = [], ranks = [], options = {}) {
10
+ const config = { ...DEFAULTS, ...options };
11
+ const nodeIds = [...new Set(nodes.map((node) => node.nodeId).filter(Boolean))].sort();
12
+ if (nodeIds.length === 0) return { width: 320, height: 160, nodes: [], edges: [] };
13
+ const known = new Set(nodeIds);
14
+ const validEdges = edges
15
+ .filter((edge) => known.has(edge.from) && known.has(edge.to) && edge.from !== edge.to)
16
+ .sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
17
+ const layers = normalizeLayers(nodeIds, validEdges, ranks);
18
+ const positioned = [];
19
+ for (let rank = 0; rank < layers.length; rank++) {
20
+ const ids = layers[rank];
21
+ for (let index = 0; index < ids.length; index++) {
22
+ positioned.push({
23
+ nodeId: ids[index], rank, index,
24
+ x: config.padding + rank * (config.nodeWidth + config.rankGap),
25
+ y: config.padding + index * (config.nodeHeight + config.nodeGap),
26
+ width: config.nodeWidth, height: config.nodeHeight,
27
+ });
28
+ }
29
+ }
30
+ const byId = new Map(positioned.map((node) => [node.nodeId, node]));
31
+ const routed = validEdges.map((edge) => {
32
+ const from = byId.get(edge.from);
33
+ const to = byId.get(edge.to);
34
+ const x1 = from.x + from.width;
35
+ const y1 = from.y + from.height / 2;
36
+ const x2 = to.x;
37
+ const y2 = to.y + to.height / 2;
38
+ const bend = Math.max(36, (x2 - x1) / 2);
39
+ return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
40
+ });
41
+ const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
42
+ return {
43
+ width: config.padding * 2 + layers.length * config.nodeWidth + (layers.length - 1) * config.rankGap,
44
+ height: config.padding * 2 + maxRows * config.nodeHeight + (maxRows - 1) * config.nodeGap,
45
+ nodes: positioned,
46
+ edges: routed,
47
+ };
48
+ }
49
+
50
+ function normalizeLayers(nodeIds, edges, ranks) {
51
+ const known = new Set(nodeIds);
52
+ const seen = new Set();
53
+ const layers = [];
54
+ if (Array.isArray(ranks)) {
55
+ for (const rank of ranks) {
56
+ if (!Array.isArray(rank)) continue;
57
+ const ids = [...new Set(rank.filter((id) => known.has(id) && !seen.has(id)))].sort();
58
+ ids.forEach((id) => seen.add(id));
59
+ if (ids.length > 0) layers.push(ids);
60
+ }
61
+ }
62
+ if (layers.length === 0 && edges.length > 0) {
63
+ const level = new Map(nodeIds.map((id) => [id, 0]));
64
+ for (let pass = 0; pass < nodeIds.length; pass++) {
65
+ let changed = false;
66
+ for (const edge of edges) {
67
+ const next = Math.min(nodeIds.length - 1, level.get(edge.from) + 1);
68
+ if (next > level.get(edge.to)) { level.set(edge.to, next); changed = true; }
69
+ }
70
+ if (!changed) break;
71
+ }
72
+ for (const id of nodeIds) {
73
+ const rank = level.get(id);
74
+ (layers[rank] ??= []).push(id);
75
+ seen.add(id);
76
+ }
77
+ for (const layer of layers) layer?.sort();
78
+ }
79
+ for (const id of nodeIds) {
80
+ if (!seen.has(id)) (layers[0] ??= []).push(id);
81
+ }
82
+ return layers.filter(Boolean);
83
+ }