@tea-agent/loop-agent 0.13.0-beta.0 → 0.14.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 -0
- package/CHANGELOG.md +56 -305
- package/README.md +13 -19
- package/dist/commands/init.js +92 -23
- package/dist/executors/pi-event-serializer.js +33 -11
- package/dist/executors/shell-executor.js +200 -21
- package/dist/infrastructure/evaluation/candidate-store.js +5 -1
- package/dist/worker/observe/spec-evidence.js +19 -10
- package/dist/worker/observe/static/app.js +4 -3
- package/dist/worker/observe/static/constants.js +10 -2
- package/dist/worker/observe/static/dag-helpers.js +37 -8
- package/dist/worker/observe/static/dom.js +159 -0
- package/dist/worker/observe/static/format-pool.d.ts +71 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +27 -2
- package/dist/worker/observe/static/index.html +76 -34
- package/dist/worker/observe/static/kpi.js +12 -6
- package/dist/worker/observe/static/markdown-render.js +124 -0
- package/dist/worker/observe/static/shell-chrome.js +8 -2
- package/dist/worker/observe/static/state.js +20 -0
- package/dist/worker/observe/static/styles.css +662 -60
- package/dist/worker/observe/static/views/dag-inspector.js +65 -142
- package/dist/worker/observe/static/views/dag.js +9 -0
- package/dist/worker/observe/static/views/dashboard.js +512 -269
- package/dist/worker/observe/static/views/pool.js +595 -237
- package/dist/worker/observe/static/views/session-timeline.js +577 -11
- package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
- package/dist/workflows/dag/backend-test-result-contract.js +568 -0
- package/dist/workflows/dag/decision-envelope.js +57 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
- package/dist/workflows/dag/frontend-project-capability.js +309 -0
- package/dist/workflows/dag/frontend-repair.js +341 -0
- package/dist/workflows/dag/frontend-risk.js +161 -0
- package/dist/workflows/dag/frontend-verification-trace.js +190 -0
- package/dist/workflows/dag/init-hybrid.js +1020 -125
- package/dist/workflows/dag/repair-artifact.js +43 -3
- package/dist/workflows/dag/skill-instructions.js +4 -2
- package/dist/workflows/dag/types.js +29 -8
- package/docs/README.md +2 -0
- package/docs/agent-dag-recovery-playbook.md +3 -3
- package/docs/agent-dag-runner.md +3 -3
- package/docs/architecture/README.md +3 -3
- package/docs/architecture/dag-execution.md +1 -1
- package/docs/architecture/evolution.md +13 -13
- package/docs/architecture/facts-and-state.md +1 -1
- package/docs/architecture/runtime-boundaries.md +7 -7
- package/docs/architecture/system-overview.md +3 -3
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/design/README.md +7 -7
- package/docs/development-principles.md +4 -4
- package/docs/exec-plans/active/README.md +2 -4
- package/docs/exec-plans/completed/README.md +29 -6
- package/docs/feature-workflow.md +57 -32
- package/docs/init-surface.manifest.json +21 -3
- package/docs/loop-agent-harness.md +8 -8
- package/docs/production-readiness.md +1 -1
- package/docs/progress/README.md +20 -3
- package/docs/reports/README.md +53 -7
- package/docs/templates/agent-dag.supervised-implementation.json +127 -8
- package/docs/templates/backend-test-case-manifest.schema.json +190 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +6 -4
- package/docs/templates/backend-test-dag.json +269 -21
- package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
- package/docs/templates/backend-test-dag.review-cases.prompt.md +6 -4
- package/docs/templates/backend-test-execution.schema.json +133 -0
- package/docs/templates/backend-test-result.schema.json +99 -0
- package/docs/templates/branch-merge-report.md +93 -0
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
- package/docs/templates/frontend-eval/metrics.md +138 -0
- package/docs/templates/frontend-eval/smoke-targets.md +53 -0
- package/docs/templates/frontend-implementation-contract.schema.json +27 -0
- package/docs/verification-matrix.md +1 -1
- package/examples/decision-gate-agent-dag.json +4 -4
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/package.json +2 -2
- package/skills/ai-engineering-context/SKILL.md +2 -2
- package/skills/browser-tools/SKILL.md +196 -0
- package/skills/browser-tools/browser-content.js +103 -0
- package/skills/browser-tools/browser-cookies.js +35 -0
- package/skills/browser-tools/browser-eval.js +53 -0
- package/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/skills/browser-tools/browser-nav.js +44 -0
- package/skills/browser-tools/browser-pick.js +162 -0
- package/skills/browser-tools/browser-screenshot.js +34 -0
- package/skills/browser-tools/browser-start.js +86 -0
- package/skills/browser-tools/package-lock.json +2556 -0
- package/skills/browser-tools/package.json +19 -0
- package/skills/frontend-implementation/SKILL.md +3 -1
- package/skills/frontend-implementation/references/node-contracts.md +17 -66
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/grill-with-docs/SKILL.md +5 -5
- package/skills/grill-with-docs/adr-format.md +3 -3
- package/skills/init-capability-evolution/SKILL.md +5 -5
- package/skills/loop-agent/SKILL.md +5 -5
- package/skills/loop-agent/references/README.md +3 -3
- package/skills/loop-agent/references/command-reference.md +39 -17
- package/skills/loop-agent/references/docs-converge.md +15 -15
- package/skills/loop-agent/references/harness-policy.md +2 -2
- package/skills/loop-agent/references/hybrid-dag.md +20 -15
- package/skills/loop-agent/references/multi-worktree.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
- package/skills/requesting-code-review/SKILL.md +1 -1
- package/skills/systematic-debugging/CREATION-LOG.md +3 -3
- package/skills/systematic-debugging/SKILL.md +1 -1
- package/skills/systematic-debugging/test-academic.md +1 -1
- package/skills/systematic-debugging/test-pressure-1.md +1 -1
- package/skills/systematic-debugging/test-pressure-2.md +1 -1
- package/skills/systematic-debugging/test-pressure-3.md +1 -1
- package/skills/verification-before-completion/SKILL.md +1 -1
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
export const ACTIVE_DETAIL_POLL_MS = 2000;
|
|
4
4
|
export const HIDDEN_POLL_MS = 30000;
|
|
5
5
|
export const DASHBOARD_POLL_MS = 5000;
|
|
6
|
-
|
|
6
|
+
/** Optional pool auto-refresh interval (default off; manual refresh preferred). */
|
|
7
|
+
export const POOL_POLL_MS = 20000;
|
|
8
|
+
/** Fallback when viewport is unavailable; open default is half viewport. */
|
|
9
|
+
export const DAG_INSPECTOR_DEFAULT_WIDTH = 720;
|
|
10
|
+
/** Open width as a fraction of the viewport (clamped to min/max). */
|
|
11
|
+
export const DAG_INSPECTOR_DEFAULT_WIDTH_RATIO = 0.5;
|
|
7
12
|
export const DAG_INSPECTOR_MIN_WIDTH = 360;
|
|
8
|
-
|
|
13
|
+
/** Absolute ceiling; effective max also respects viewport margin. */
|
|
14
|
+
export const DAG_INSPECTOR_MAX_WIDTH = 1200;
|
|
9
15
|
export const DAG_INSPECTOR_RIGHT_OFFSET = 24;
|
|
10
16
|
export const DAG_INSPECTOR_VIEWPORT_MARGIN = 48;
|
|
11
17
|
export const DAG_INSPECTOR_KEYBOARD_STEP = 24;
|
|
@@ -37,6 +43,8 @@ export const UI_TEXT = {
|
|
|
37
43
|
refreshStatus: "更新",
|
|
38
44
|
failureInbox: "异常 Task",
|
|
39
45
|
nearRealtime: "轮询刷新",
|
|
46
|
+
manualRefresh: "手动刷新",
|
|
47
|
+
autoRefresh: "自动刷新",
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
export const STATUS_LABELS = {
|
|
@@ -48,16 +48,45 @@ export function getActiveNodes(dag) {
|
|
|
48
48
|
return (dag.nodes ?? []).filter((n) => isNodeActive(n.status));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function dagSortTime(dag) {
|
|
52
|
-
|
|
53
|
-
}
|
|
51
|
+
export function dagSortTime(dag) {
|
|
52
|
+
return dag.startedAt ?? dag.finishedAt ?? "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Visual urgency for active DAG cards: attention > running > idle. */
|
|
56
|
+
export function dagCardUrgency(dag) {
|
|
57
|
+
const status = (dag?.effectiveStatus ?? "").toLowerCase();
|
|
58
|
+
const liveness = (dag?.liveness ?? "").toLowerCase();
|
|
59
|
+
const lifecycle = (dag?.lifecycle ?? "").toLowerCase();
|
|
60
|
+
if (
|
|
61
|
+
lifecycle === "paused" ||
|
|
62
|
+
dag?.stateConsistent === false ||
|
|
63
|
+
["interrupted", "remote-unknown"].includes(status) ||
|
|
64
|
+
["stale", "orphaned"].includes(liveness)
|
|
65
|
+
) {
|
|
66
|
+
return "attention";
|
|
67
|
+
}
|
|
68
|
+
if (isNodeActive(dag?.status) || getActiveNodes(dag).length > 0) {
|
|
69
|
+
return "running";
|
|
70
|
+
}
|
|
71
|
+
return "idle";
|
|
72
|
+
}
|
|
54
73
|
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
export function compareActiveDagUrgency(a, b) {
|
|
75
|
+
const rank = { attention: 0, running: 1, idle: 2 };
|
|
76
|
+
const diff = (rank[dagCardUrgency(a)] ?? 2) - (rank[dagCardUrgency(b)] ?? 2);
|
|
77
|
+
if (diff !== 0) return diff;
|
|
78
|
+
return dagSortTime(b).localeCompare(dagSortTime(a));
|
|
59
79
|
}
|
|
60
|
-
|
|
80
|
+
|
|
81
|
+
export function renderActiveDagCard(dag, nowMs) {
|
|
82
|
+
const card = el("div", "active-dag-card");
|
|
83
|
+
const urgency = dagCardUrgency(dag);
|
|
84
|
+
if (urgency === "attention") {
|
|
85
|
+
card.classList.add("active-dag-card-attention");
|
|
86
|
+
} else if (urgency === "running") {
|
|
87
|
+
card.classList.add("active-dag-card-running");
|
|
88
|
+
}
|
|
89
|
+
const title = dag.title || dag.dagRunId;
|
|
61
90
|
const header = el("div", "active-dag-header");
|
|
62
91
|
header.appendChild(el("span", "active-dag-title", title));
|
|
63
92
|
header.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
@@ -1,5 +1,164 @@
|
|
|
1
1
|
/** DOM helpers (Observe UI R5). */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Stable key → node maps, kept per (parent, scope) so reconcileKeyed never
|
|
5
|
+
* depends on dataset support. A WeakMap keyed by parent holds a map of
|
|
6
|
+
* scope → Map<key, node>. A stale parent (e.g. after view teardown) releases
|
|
7
|
+
* its bookkeeping automatically. The default scope "default" is used when a
|
|
8
|
+
* caller reconciles all keyed children of a parent in a single pass; multiple
|
|
9
|
+
* reconcilers that share one parent must pass distinct scope strings so they
|
|
10
|
+
* do not clobber each other's keys.
|
|
11
|
+
*/
|
|
12
|
+
const reconcileMaps = new WeakMap();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Keyed DOM reconciliation: preserve node identity when a stable key recurs
|
|
16
|
+
* across renders; create a fresh node (with fresh closures) when the key is
|
|
17
|
+
* new; remove nodes whose key dropped out of the item set.
|
|
18
|
+
*
|
|
19
|
+
* Callers drive node construction via `create` and content sync via optional
|
|
20
|
+
* `update`. The parent is not bulk-cleared, so headings/headers placed outside
|
|
21
|
+
* this call survive, and scrolling/focus/pointer state on reused nodes is kept.
|
|
22
|
+
*
|
|
23
|
+
* @param {Node} parent
|
|
24
|
+
* @param {Array} items
|
|
25
|
+
* @param {{ getKey: (item: any, index: number) => string,
|
|
26
|
+
* create: (item: any, index: number) => Node,
|
|
27
|
+
* update?: (node: any, item: any, index: number) => void,
|
|
28
|
+
* scope?: string }} opts
|
|
29
|
+
* @returns {Array<Node>} the (possibly reused) nodes in item order
|
|
30
|
+
*/
|
|
31
|
+
export function reconcileKeyed(parent, items, opts) {
|
|
32
|
+
const { getKey, create, update, scope = "default" } = opts;
|
|
33
|
+
let scopes = reconcileMaps.get(parent);
|
|
34
|
+
if (!scopes) {
|
|
35
|
+
scopes = new Map();
|
|
36
|
+
reconcileMaps.set(parent, scopes);
|
|
37
|
+
}
|
|
38
|
+
let map = scopes.get(scope);
|
|
39
|
+
if (!map) {
|
|
40
|
+
map = new Map();
|
|
41
|
+
scopes.set(scope, map);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nextKeys = new Set();
|
|
45
|
+
const result = [];
|
|
46
|
+
for (let i = 0; i < items.length; i++) {
|
|
47
|
+
const item = items[i];
|
|
48
|
+
const key = String(getKey(item, i));
|
|
49
|
+
nextKeys.add(key);
|
|
50
|
+
let node = map.get(key);
|
|
51
|
+
if (node) {
|
|
52
|
+
if (update) update(node, item, i);
|
|
53
|
+
} else {
|
|
54
|
+
node = create(item, i);
|
|
55
|
+
map.set(key, node);
|
|
56
|
+
// Sync freshly created nodes so callers can split structure (create) from
|
|
57
|
+
// content (update) and still populate the first paint.
|
|
58
|
+
if (update) update(node, item, i);
|
|
59
|
+
}
|
|
60
|
+
result.push(node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove nodes whose key is no longer present in THIS scope.
|
|
64
|
+
for (const [key, node] of map) {
|
|
65
|
+
if (!nextKeys.has(key)) {
|
|
66
|
+
parent.removeChild(node);
|
|
67
|
+
map.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resultSet = new Set(result);
|
|
72
|
+
const currentOrder = childNodesOf(parent).filter((node) => resultSet.has(node));
|
|
73
|
+
const orderChanged =
|
|
74
|
+
currentOrder.length !== result.length ||
|
|
75
|
+
currentOrder.some((node, index) => node !== result[index]);
|
|
76
|
+
if (orderChanged) {
|
|
77
|
+
for (const node of result) parent.appendChild(node);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function childNodesOf(node) {
|
|
84
|
+
return Array.from(node.childNodes ?? node.children ?? []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function nodeKind(node) {
|
|
88
|
+
if (node.nodeType === 3) return "#text";
|
|
89
|
+
return node.nodeName ?? node.tagName ?? "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function syncElementAttributes(current, next) {
|
|
93
|
+
if (current.getAttributeNames && next.getAttributeNames) {
|
|
94
|
+
const nextNames = new Set(next.getAttributeNames());
|
|
95
|
+
for (const name of current.getAttributeNames()) {
|
|
96
|
+
if (!nextNames.has(name)) current.removeAttribute(name);
|
|
97
|
+
}
|
|
98
|
+
for (const name of nextNames) {
|
|
99
|
+
current.setAttribute(name, next.getAttribute(name));
|
|
100
|
+
}
|
|
101
|
+
} else if (current.attrs && next.attrs) {
|
|
102
|
+
for (const name of Object.keys(current.attrs)) {
|
|
103
|
+
if (!(name in next.attrs)) delete current.attrs[name];
|
|
104
|
+
}
|
|
105
|
+
for (const [name, value] of Object.entries(next.attrs)) {
|
|
106
|
+
current.setAttribute(name, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const prop of [
|
|
111
|
+
"className",
|
|
112
|
+
"href",
|
|
113
|
+
"title",
|
|
114
|
+
"target",
|
|
115
|
+
"type",
|
|
116
|
+
"value",
|
|
117
|
+
"hidden",
|
|
118
|
+
"tabIndex",
|
|
119
|
+
]) {
|
|
120
|
+
if (prop in next) current[prop] = next[prop];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Recursively sync a structurally compatible node without replacing compatible
|
|
126
|
+
* descendants. Existing event listeners and focusable node identity survive;
|
|
127
|
+
* incompatible tag/node kinds are reported to the caller for replacement.
|
|
128
|
+
*/
|
|
129
|
+
export function syncCompatibleNode(current, next) {
|
|
130
|
+
if (!current || !next || nodeKind(current) !== nodeKind(next)) return false;
|
|
131
|
+
if (current.nodeType === 3 || nodeKind(current) === "#TEXT") {
|
|
132
|
+
current.textContent = next.textContent;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
syncElementAttributes(current, next);
|
|
137
|
+
const currentChildren = childNodesOf(current);
|
|
138
|
+
const nextChildren = childNodesOf(next);
|
|
139
|
+
if (nextChildren.length === 0) {
|
|
140
|
+
current.textContent = next.textContent;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (let index = 0; index < Math.max(currentChildren.length, nextChildren.length); index++) {
|
|
145
|
+
const currentChild = currentChildren[index];
|
|
146
|
+
const nextChild = nextChildren[index];
|
|
147
|
+
if (!nextChild && currentChild) {
|
|
148
|
+
current.removeChild(currentChild);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (nextChild && !currentChild) {
|
|
152
|
+
current.appendChild(nextChild);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!syncCompatibleNode(currentChild, nextChild)) {
|
|
156
|
+
current.replaceChild(nextChild, currentChild);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
3
162
|
export function clearNode(node) {
|
|
4
163
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
5
164
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type PoolTaskLike = {
|
|
2
|
+
taskId?: string;
|
|
3
|
+
featureId?: string;
|
|
4
|
+
status?: string;
|
|
5
|
+
liveness?: string;
|
|
6
|
+
updatedAt?: string;
|
|
7
|
+
finishedAt?: string;
|
|
8
|
+
startedAt?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
priority?: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
recommendedFollowUp?: string;
|
|
14
|
+
failureCategory?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PoolStatusCounts = {
|
|
19
|
+
running: number;
|
|
20
|
+
pending: number;
|
|
21
|
+
blocked: number;
|
|
22
|
+
failed: number;
|
|
23
|
+
succeeded: number;
|
|
24
|
+
other: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PlannerReadyItem = {
|
|
28
|
+
featureId: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
priority?: string;
|
|
32
|
+
type?: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type FeaturePlanningLike = {
|
|
37
|
+
featureId?: string;
|
|
38
|
+
planning?: {
|
|
39
|
+
selected?: Array<{
|
|
40
|
+
featureId?: string;
|
|
41
|
+
taskId?: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
priority?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
reason?: string;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function sortPoolTasks(tasks: PoolTaskLike[] | null | undefined): PoolTaskLike[];
|
|
51
|
+
export function matchesPoolStatusFilter(
|
|
52
|
+
taskStatus: string | null | undefined,
|
|
53
|
+
filter: string | null | undefined,
|
|
54
|
+
): boolean;
|
|
55
|
+
export function poolStatusCounts(
|
|
56
|
+
tasks: Array<{ status?: string }> | null | undefined,
|
|
57
|
+
): PoolStatusCounts;
|
|
58
|
+
export function isPoolAttentionTask(
|
|
59
|
+
task: PoolTaskLike | null | undefined,
|
|
60
|
+
): boolean;
|
|
61
|
+
export function attentionRank(task: PoolTaskLike | null | undefined): number;
|
|
62
|
+
export function sortAttentionTasks(
|
|
63
|
+
tasks: PoolTaskLike[] | null | undefined,
|
|
64
|
+
): PoolTaskLike[];
|
|
65
|
+
export function collectPlannerReadyTasks(
|
|
66
|
+
features: FeaturePlanningLike[] | null | undefined,
|
|
67
|
+
): PlannerReadyItem[];
|
|
68
|
+
export function taskIdentityKey(
|
|
69
|
+
featureId: string | null | undefined,
|
|
70
|
+
taskId: string | null | undefined,
|
|
71
|
+
): string;
|
|
@@ -65,3 +65,70 @@ export function poolStatusCounts(tasks) {
|
|
|
65
65
|
}
|
|
66
66
|
return counts;
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/** Tasks that need operator attention on the pool ops surface. */
|
|
70
|
+
export function isPoolAttentionTask(task) {
|
|
71
|
+
const st = (task?.status ?? "").toLowerCase();
|
|
72
|
+
const live = (task?.liveness ?? "").toLowerCase();
|
|
73
|
+
if (["failed", "error", "blocked", "stale"].includes(st)) return true;
|
|
74
|
+
if (["stale", "orphaned", "timeout-risk"].includes(live)) return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function attentionRank(task) {
|
|
79
|
+
const st = (task?.status ?? "").toLowerCase();
|
|
80
|
+
const live = (task?.liveness ?? "").toLowerCase();
|
|
81
|
+
if (st === "failed" || st === "error") return 0;
|
|
82
|
+
if (live === "timeout-risk") return 1;
|
|
83
|
+
if (st === "stale" || live === "stale" || live === "orphaned") return 2;
|
|
84
|
+
if (st === "blocked") return 3;
|
|
85
|
+
return 9;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function sortAttentionTasks(tasks) {
|
|
89
|
+
const list = (tasks ?? []).filter(isPoolAttentionTask);
|
|
90
|
+
list.sort((a, b) => {
|
|
91
|
+
const diff = attentionRank(a) - attentionRank(b);
|
|
92
|
+
if (diff !== 0) return diff;
|
|
93
|
+
const ta = a.updatedAt || a.finishedAt || a.startedAt || "";
|
|
94
|
+
const tb = b.updatedAt || b.finishedAt || b.startedAt || "";
|
|
95
|
+
return tb.localeCompare(ta);
|
|
96
|
+
});
|
|
97
|
+
return list;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Collect Ready Planner `selected` across features.
|
|
102
|
+
* Preserves snapshot feature order and each feature's planner-selected order.
|
|
103
|
+
* Does not re-rank Ready tasks on the client.
|
|
104
|
+
*/
|
|
105
|
+
export function collectPlannerReadyTasks(features) {
|
|
106
|
+
const out = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const feature of features ?? []) {
|
|
109
|
+
const featureId = feature?.featureId;
|
|
110
|
+
const selected = feature?.planning?.selected ?? [];
|
|
111
|
+
for (const item of selected) {
|
|
112
|
+
const taskId = item?.taskId;
|
|
113
|
+
if (!taskId) continue;
|
|
114
|
+
const fid = item.featureId ?? featureId;
|
|
115
|
+
if (!fid) continue;
|
|
116
|
+
const key = `${fid}\0${taskId}`;
|
|
117
|
+
if (seen.has(key)) continue;
|
|
118
|
+
seen.add(key);
|
|
119
|
+
out.push({
|
|
120
|
+
featureId: fid,
|
|
121
|
+
taskId,
|
|
122
|
+
title: item.title,
|
|
123
|
+
priority: item.priority,
|
|
124
|
+
type: item.type,
|
|
125
|
+
reason: item.reason ?? "dependencies-satisfied",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function taskIdentityKey(featureId, taskId) {
|
|
133
|
+
return `${featureId ?? ""}\0${taskId ?? ""}`;
|
|
134
|
+
}
|
|
@@ -16,7 +16,22 @@ export function parseMarkdownBlocks(markdown) {
|
|
|
16
16
|
let code = null;
|
|
17
17
|
|
|
18
18
|
const flushParagraph = () => {
|
|
19
|
-
|
|
19
|
+
if (paragraph.length === 0) return;
|
|
20
|
+
// Preserve Markdown hard breaks (trailing " " or "\") as \n segments.
|
|
21
|
+
// Soft-wrapped lines still join with a space.
|
|
22
|
+
let text = "";
|
|
23
|
+
let prevHardBreak = false;
|
|
24
|
+
for (let i = 0; i < paragraph.length; i += 1) {
|
|
25
|
+
const raw = paragraph[i] ?? "";
|
|
26
|
+
const hardBreak = / {2}$/.test(raw) || /\\$/.test(raw);
|
|
27
|
+
const cleaned = raw.replace(/(?: {2}|\\)$/, "").trim();
|
|
28
|
+
if (!cleaned && !hardBreak) continue;
|
|
29
|
+
if (i === 0 || text === "") text = cleaned;
|
|
30
|
+
else if (prevHardBreak) text += `\n${cleaned}`;
|
|
31
|
+
else text += ` ${cleaned}`;
|
|
32
|
+
prevHardBreak = hardBreak;
|
|
33
|
+
}
|
|
34
|
+
text = text.trim();
|
|
20
35
|
if (text) blocks.push({ type: "paragraph", text });
|
|
21
36
|
paragraph = [];
|
|
22
37
|
};
|
|
@@ -114,7 +129,8 @@ export function parseMarkdownBlocks(markdown) {
|
|
|
114
129
|
continue;
|
|
115
130
|
}
|
|
116
131
|
flushList();
|
|
117
|
-
|
|
132
|
+
// Keep trailing hard-break markers; trim only leading indent.
|
|
133
|
+
paragraph.push(line.replace(/^\s+/, ""));
|
|
118
134
|
}
|
|
119
135
|
if (code)
|
|
120
136
|
blocks.push({
|
|
@@ -290,3 +306,12 @@ export function repoBaseName(repoRoot) {
|
|
|
290
306
|
return parts[parts.length - 1] || trimmed || "未绑定仓库";
|
|
291
307
|
}
|
|
292
308
|
|
|
309
|
+
/** Compact path for header display; full path stays in title/tooltip. */
|
|
310
|
+
export function formatRepoPathDisplay(repoRoot) {
|
|
311
|
+
if (!repoRoot) return "路径未知";
|
|
312
|
+
return String(repoRoot)
|
|
313
|
+
.replace(/\\/g, "/")
|
|
314
|
+
.replace(/^\/Users\/[^/]+/, "~")
|
|
315
|
+
.replace(/^\/home\/[^/]+/, "~");
|
|
316
|
+
}
|
|
317
|
+
|
|
@@ -51,21 +51,17 @@
|
|
|
51
51
|
id="view-dashboard"
|
|
52
52
|
class="view view-dashboard"
|
|
53
53
|
>
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
id="dashboard-features"
|
|
66
|
-
class="panel panel-features"
|
|
67
|
-
aria-label="Feature 决策"
|
|
68
|
-
></section>
|
|
54
|
+
<header class="page-header dashboard-page-header">
|
|
55
|
+
<div class="view-heading page-header-heading">
|
|
56
|
+
<p class="view-kicker">OBSERVE</p>
|
|
57
|
+
<h2>运行总览</h2>
|
|
58
|
+
</div>
|
|
59
|
+
<div
|
|
60
|
+
id="dashboard-repo"
|
|
61
|
+
class="repo-banner repo-banner-inline"
|
|
62
|
+
aria-label="当前仓库"
|
|
63
|
+
></div>
|
|
64
|
+
</header>
|
|
69
65
|
<section
|
|
70
66
|
id="dashboard-kpi"
|
|
71
67
|
class="panel panel-kpi"
|
|
@@ -78,10 +74,15 @@
|
|
|
78
74
|
></section>
|
|
79
75
|
<aside
|
|
80
76
|
id="dashboard-risk"
|
|
81
|
-
class="panel panel-risk"
|
|
77
|
+
class="panel panel-primary panel-risk"
|
|
82
78
|
aria-label="运行风险"
|
|
83
79
|
></aside>
|
|
84
80
|
</div>
|
|
81
|
+
<section
|
|
82
|
+
id="dashboard-features"
|
|
83
|
+
class="panel panel-features"
|
|
84
|
+
aria-label="Feature 决策"
|
|
85
|
+
></section>
|
|
85
86
|
<div class="dashboard-secondary">
|
|
86
87
|
<section id="dashboard-dags" class="panel"></section>
|
|
87
88
|
<section id="dashboard-batches" class="panel"></section>
|
|
@@ -189,38 +190,79 @@
|
|
|
189
190
|
<section id="failures-list" class="panel panel-table"></section>
|
|
190
191
|
</section>
|
|
191
192
|
|
|
192
|
-
<section data-view="pool" id="view-pool" class="view" hidden>
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
193
|
+
<section data-view="pool" id="view-pool" class="view view-pool" hidden>
|
|
194
|
+
<header class="page-header pool-page-header">
|
|
195
|
+
<div class="view-heading page-header-heading">
|
|
196
|
+
<p class="view-kicker">TASK POOL</p>
|
|
197
|
+
<h2>资源池总览</h2>
|
|
198
|
+
</div>
|
|
199
|
+
<div id="pool-toolbar" class="pool-toolbar" aria-label="刷新控制">
|
|
200
|
+
<div class="pool-toolbar-actions">
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
id="pool-refresh-btn"
|
|
204
|
+
class="pool-refresh-btn"
|
|
205
|
+
>
|
|
206
|
+
刷新
|
|
207
|
+
</button>
|
|
208
|
+
<label class="pool-auto-refresh-label">
|
|
209
|
+
<input type="checkbox" id="pool-auto-refresh" />
|
|
210
|
+
自动刷新
|
|
211
|
+
</label>
|
|
212
|
+
<span id="pool-refresh-status" class="muted"></span>
|
|
213
|
+
</div>
|
|
214
|
+
<span
|
|
215
|
+
id="pool-updated-at"
|
|
216
|
+
class="muted pool-updated-at"
|
|
217
|
+
aria-live="polite"
|
|
218
|
+
></span>
|
|
219
|
+
</div>
|
|
220
|
+
</header>
|
|
201
221
|
<section
|
|
202
222
|
id="pool-fault"
|
|
203
|
-
class="panel"
|
|
223
|
+
class="panel panel-meta"
|
|
204
224
|
aria-label="投影告警"
|
|
225
|
+
hidden
|
|
205
226
|
></section>
|
|
206
227
|
<section
|
|
207
228
|
id="pool-kpi"
|
|
208
229
|
class="panel panel-kpi"
|
|
209
|
-
aria-label="
|
|
230
|
+
aria-label="池健康"
|
|
210
231
|
></section>
|
|
232
|
+
<div class="pool-workspace">
|
|
233
|
+
<section
|
|
234
|
+
id="pool-attention"
|
|
235
|
+
class="panel panel-primary"
|
|
236
|
+
aria-label="需处理"
|
|
237
|
+
></section>
|
|
238
|
+
<section
|
|
239
|
+
id="pool-ready"
|
|
240
|
+
class="panel panel-primary"
|
|
241
|
+
aria-label="Ready"
|
|
242
|
+
></section>
|
|
243
|
+
</div>
|
|
211
244
|
<div id="pool-empty" class="panel empty-state" hidden></div>
|
|
245
|
+
<div
|
|
246
|
+
id="pool-filters"
|
|
247
|
+
class="pool-filters"
|
|
248
|
+
aria-label="清单筛选"
|
|
249
|
+
></div>
|
|
212
250
|
<section
|
|
213
251
|
id="pool-tasks"
|
|
214
252
|
class="panel panel-table"
|
|
215
|
-
aria-label="
|
|
216
|
-
></section>
|
|
217
|
-
<section
|
|
218
|
-
id="pool-events"
|
|
219
|
-
class="panel panel-timeline"
|
|
220
|
-
aria-label="最近事件"
|
|
253
|
+
aria-label="全部 Task"
|
|
221
254
|
></section>
|
|
255
|
+
<details id="pool-events-details" class="pool-events-details">
|
|
256
|
+
<summary>最近事件(可选,默认折叠)</summary>
|
|
257
|
+
<section
|
|
258
|
+
id="pool-events"
|
|
259
|
+
class="panel panel-timeline"
|
|
260
|
+
aria-label="最近事件"
|
|
261
|
+
></section>
|
|
262
|
+
</details>
|
|
222
263
|
<p class="panel muted advisory-note" id="pool-advisory">
|
|
223
|
-
数据为本地 derived/advisory 投影,仅供运营参考,非权威写路径。
|
|
264
|
+
数据为本地 derived/advisory 投影,仅供运营参考,非权威写路径。Ready
|
|
265
|
+
列表与 Ready Planner 同源,前端不重排。
|
|
224
266
|
</p>
|
|
225
267
|
</section>
|
|
226
268
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** KPI cards, repo banner, projection fault, artifact/inbox links. */
|
|
2
2
|
import { UI_TEXT } from "./constants.js";
|
|
3
3
|
import { clearNode, el } from "./dom.js";
|
|
4
|
-
import { repoBaseName } from "./format.js";
|
|
4
|
+
import { formatRepoPathDisplay, repoBaseName } from "./format.js";
|
|
5
5
|
import { navigate } from "./router.js";
|
|
6
6
|
|
|
7
7
|
export function failuresInboxLink(count) {
|
|
8
|
-
const wrap = el("span");
|
|
9
|
-
wrap.appendChild(
|
|
8
|
+
const wrap = el("span", "kpi-value-with-link");
|
|
9
|
+
wrap.appendChild(el("span", "kpi-value-num", String(count)));
|
|
10
10
|
const a = el("a", "inbox-link", `${UI_TEXT.failureInbox} →`);
|
|
11
11
|
a.href = "#/failures";
|
|
12
12
|
a.addEventListener("click", (e) => {
|
|
@@ -59,13 +59,19 @@ export function renderRepoBanner(repoEl, snapshot) {
|
|
|
59
59
|
if (!repoEl) return;
|
|
60
60
|
clearNode(repoEl);
|
|
61
61
|
const repoRoot = snapshot.repoRoot ?? "";
|
|
62
|
+
const iconWrap = el("span", "repo-banner-icon");
|
|
62
63
|
const icon = el("i", "ri-git-repository-line");
|
|
63
64
|
icon.setAttribute("aria-hidden", "true");
|
|
64
|
-
|
|
65
|
+
iconWrap.appendChild(icon);
|
|
66
|
+
repoEl.appendChild(iconWrap);
|
|
65
67
|
const text = el("div", "repo-banner-text");
|
|
66
68
|
text.appendChild(el("span", "repo-banner-name", repoBaseName(repoRoot)));
|
|
67
|
-
const pathLine = el(
|
|
68
|
-
|
|
69
|
+
const pathLine = el(
|
|
70
|
+
"span",
|
|
71
|
+
"repo-banner-path",
|
|
72
|
+
formatRepoPathDisplay(repoRoot),
|
|
73
|
+
);
|
|
74
|
+
pathLine.title = repoRoot || "路径未知";
|
|
69
75
|
text.appendChild(pathLine);
|
|
70
76
|
repoEl.appendChild(text);
|
|
71
77
|
}
|