@tea-agent/loop-agent 0.25.5 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +1020 -986
- package/bin/loop-agent.js +21 -21
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/init.js +68 -129
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/dag-pi-executor.js +26 -20
- package/dist/executors/model-routing.js +34 -18
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/manifest-types.js +33 -5
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/task-demand-routing.js +3 -1
- package/dist/worker/console/chat/model-resolver.js +15 -3
- package/dist/worker/observe/static/constants.js +3 -2
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dag-model.js +1 -0
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/styles.css +182 -42
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +362 -45
- package/dist/workflows/dag/backend-test-case-manifest.js +20 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/init-hybrid.js +18 -15
- package/dist/workflows/dag/lifecycle.js +40 -30
- package/dist/workflows/dag/node-execution.js +13 -0
- package/dist/workflows/dag/types.js +59 -19
- package/docs/init-surface.manifest.json +3 -1
- package/docs/skills/README.md +7 -7
- package/docs/templates/README.md +1 -0
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-dag.json +9 -9
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/harness.schema.json +29 -7
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +137 -0
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/harness.json +1 -2
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +5 -4
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/model-routing.md +2 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -18,6 +18,34 @@ export const worktreeManifestConfigSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
export const taskExecutorSchema = z.enum(["pi"]);
|
|
20
20
|
export const CURSOR_TASK_EXECUTOR_REMOVED_ERROR = 'task executor "cursor" is no longer supported; governed runtime is Pi-only';
|
|
21
|
+
/** Per-tier model override: bare model id string, or `{ model, thinking? }`. */
|
|
22
|
+
export const executorTierModelSchema = z.union([
|
|
23
|
+
z.string(),
|
|
24
|
+
z.object({
|
|
25
|
+
model: z.string().min(1),
|
|
26
|
+
thinking: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
/** Normalize an executors.pi LOW|MED|HIGH value. */
|
|
30
|
+
export function normalizeExecutorTierValue(value) {
|
|
31
|
+
if (value === null || value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
if (value === "default" || value.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
return { model: value };
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
39
|
+
return undefined;
|
|
40
|
+
const model = value.model;
|
|
41
|
+
if (typeof model !== "string" || model.length === 0 || model === "default") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const thinking = value.thinking;
|
|
45
|
+
return typeof thinking === "string" && thinking.length > 0
|
|
46
|
+
? { model, thinking }
|
|
47
|
+
: { model };
|
|
48
|
+
}
|
|
21
49
|
export const executorManifestSchema = z.object({
|
|
22
50
|
description: z.string().optional(),
|
|
23
51
|
enabled: z.boolean().optional(),
|
|
@@ -27,9 +55,9 @@ export const executorManifestSchema = z.object({
|
|
|
27
55
|
* these take priority over defaultModel for the matching DAG executor tier.
|
|
28
56
|
* "default" literal and absent/undefined both mean "no override, fall through".
|
|
29
57
|
*/
|
|
30
|
-
LOW:
|
|
31
|
-
MED:
|
|
32
|
-
HIGH:
|
|
58
|
+
LOW: executorTierModelSchema.optional(),
|
|
59
|
+
MED: executorTierModelSchema.optional(),
|
|
60
|
+
HIGH: executorTierModelSchema.optional(),
|
|
33
61
|
requiresApiKey: z.string().optional(),
|
|
34
62
|
});
|
|
35
63
|
export const workflowPolicyProfileNameSchema = z.enum([
|
|
@@ -98,7 +126,7 @@ export const workflowPolicySchema = z
|
|
|
98
126
|
})
|
|
99
127
|
.optional()
|
|
100
128
|
.default({});
|
|
101
|
-
export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR =
|
|
129
|
+
export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
|
|
102
130
|
export const harnessManifestSchema = z
|
|
103
131
|
.object({
|
|
104
132
|
version: z.number(),
|
|
@@ -160,7 +188,7 @@ export const harnessManifestSchema = z
|
|
|
160
188
|
if ("cursorExecutorUsage" in entrypoints) {
|
|
161
189
|
ctx.addIssue({
|
|
162
190
|
code: z.ZodIssueCode.custom,
|
|
163
|
-
message:
|
|
191
|
+
message: "entrypoints.cursorExecutorUsage is no longer supported; remove it (Cursor is cursor-prompt sidecar only)",
|
|
164
192
|
path: ["entrypoints", "cursorExecutorUsage"],
|
|
165
193
|
});
|
|
166
194
|
}
|
|
@@ -29,7 +29,7 @@ export function resolveArtifactWriteDir(options) {
|
|
|
29
29
|
}
|
|
30
30
|
export function buildArtifactPathPrompt(writeDir) {
|
|
31
31
|
if (!writeDir)
|
|
32
|
-
return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
|
|
32
|
+
return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
|
|
33
33
|
${ARTIFACT_INSTRUCTIONS}`;
|
|
34
34
|
return [
|
|
35
35
|
`After changes, write the following files:`,
|
|
@@ -156,7 +156,7 @@ function isDocumentationOnlyClause(clause) {
|
|
|
156
156
|
if (DOCUMENTATION_ONLY_MARKERS.test(clause)) {
|
|
157
157
|
return !PRODUCT_AND_DOCUMENT_DELIVERY.test(clause);
|
|
158
158
|
}
|
|
159
|
-
return TEST_ONLY_MARKERS.test(clause) && !PRODUCT_AND_TEST_DELIVERY.test(clause);
|
|
159
|
+
return (TEST_ONLY_MARKERS.test(clause) && !PRODUCT_AND_TEST_DELIVERY.test(clause));
|
|
160
160
|
}
|
|
161
161
|
function masksExistingBackendDependency(clause) {
|
|
162
162
|
return clause
|
|
@@ -335,6 +335,8 @@ export function classifyTaskDemand(input) {
|
|
|
335
335
|
}
|
|
336
336
|
const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
|
|
337
337
|
const frontendProjectDefaultImplementation = hasStrongFrontendProjectEvidence &&
|
|
338
|
+
frontendPath &&
|
|
339
|
+
!hasBackendTaskType &&
|
|
338
340
|
!backendDelivery &&
|
|
339
341
|
!frontendNegated &&
|
|
340
342
|
!allowedPathsOnlyCoverNonProductArtifacts;
|
|
@@ -20,6 +20,18 @@ import { readFile } from "node:fs/promises";
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
/** Tier keys in harness executors.pi that may override the default. */
|
|
22
22
|
const EXECUTOR_TIERS = ["LOW", "MED", "HIGH"];
|
|
23
|
+
function modelIdFromTierValue(value) {
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
return value && value !== "default" ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const model = value.model;
|
|
31
|
+
return typeof model === "string" && model && model !== "default"
|
|
32
|
+
? model
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
23
35
|
/**
|
|
24
36
|
* Read executors.pi.<tier> from the repo harness.json. Returns undefined when
|
|
25
37
|
* harness.json is absent or the tier is unset / "default" sentinel.
|
|
@@ -33,9 +45,9 @@ export async function readHarnessPiModelTier(repoRoot, tier = "MED") {
|
|
|
33
45
|
const pi = manifest.executors?.pi;
|
|
34
46
|
if (!pi)
|
|
35
47
|
return undefined;
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
48
|
+
const tierModel = modelIdFromTierValue(pi[tier]);
|
|
49
|
+
if (tierModel)
|
|
50
|
+
return tierModel;
|
|
39
51
|
const defaultModel = pi.defaultModel;
|
|
40
52
|
if (defaultModel && defaultModel !== "default")
|
|
41
53
|
return defaultModel;
|
|
@@ -67,8 +67,8 @@ export const STATUS_LABELS = {
|
|
|
67
67
|
done: "完成",
|
|
68
68
|
failed: "失败",
|
|
69
69
|
error: "错误",
|
|
70
|
-
partial_failed: "
|
|
71
|
-
partialfailed: "
|
|
70
|
+
partial_failed: "部分成功",
|
|
71
|
+
partialfailed: "部分成功",
|
|
72
72
|
blocked: "阻塞",
|
|
73
73
|
stale: "心跳失联",
|
|
74
74
|
reused: "复用",
|
|
@@ -104,6 +104,7 @@ export const DAG_EFFECTIVE_STATUS_LABELS = {
|
|
|
104
104
|
interrupted: "执行已中断",
|
|
105
105
|
"remote-unknown": "远端状态未知",
|
|
106
106
|
finished: "已完成",
|
|
107
|
+
partial_failed: "部分成功",
|
|
107
108
|
failed: "执行失败",
|
|
108
109
|
superseded: "任务已另行完成",
|
|
109
110
|
abandoned: "已放弃",
|
|
@@ -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
|
+
}
|