@unbrained/pm-web 2026.6.9 → 2026.6.13-2
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/CHANGELOG.md +27 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.js +14 -2
- package/dist/server.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +8 -6
- package/public/src/app.js +3 -1
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +3 -1
- package/public/src/views/plan-execution.js +191 -0
- package/public/src/views/plan-execution.js.map +1 -0
- package/public/src/views/plan-execution.ts +279 -0
- package/public/src/views/plan.js +267 -29
- package/public/src/views/plan.js.map +1 -1
- package/public/src/views/plan.ts +293 -29
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
type PlanStepInput = {
|
|
2
|
+
id?: string;
|
|
3
|
+
ref?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
blockedReason?: string;
|
|
8
|
+
blocked_reason?: string;
|
|
9
|
+
dependsOn?: string[];
|
|
10
|
+
depends_on?: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PlanInput = {
|
|
14
|
+
id?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
scope?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type AnalyzedPlanStep = {
|
|
22
|
+
ref: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
status: string;
|
|
26
|
+
blockedReason: string;
|
|
27
|
+
dependsOn: string[];
|
|
28
|
+
unresolvedDependencies: string[];
|
|
29
|
+
incompleteDependencies: string[];
|
|
30
|
+
isDone: boolean;
|
|
31
|
+
isBlocked: boolean;
|
|
32
|
+
original: PlanStepInput;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type PlanExecutionSnapshot = {
|
|
36
|
+
totalSteps: number;
|
|
37
|
+
completedSteps: number;
|
|
38
|
+
blockedSteps: number;
|
|
39
|
+
waitingSteps: AnalyzedPlanStep[];
|
|
40
|
+
readySteps: AnalyzedPlanStep[];
|
|
41
|
+
blockedStepDetails: AnalyzedPlanStep[];
|
|
42
|
+
allSteps: AnalyzedPlanStep[];
|
|
43
|
+
stepByRef: Record<string, AnalyzedPlanStep>;
|
|
44
|
+
completionPct: number;
|
|
45
|
+
nextReadyStep: AnalyzedPlanStep | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const DONE_STATUSES = new Set(['done', 'completed', 'closed']);
|
|
49
|
+
const BLOCKED_STATUSES = new Set(['blocked']);
|
|
50
|
+
|
|
51
|
+
function normalizeStatus(status?: string): string {
|
|
52
|
+
const value = (status || 'pending').trim().toLowerCase().replace(/\s+/g, '_');
|
|
53
|
+
return value || 'pending';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeStepRef(step: PlanStepInput, index: number): string {
|
|
57
|
+
const candidate = (step.ref || step.id || '').trim();
|
|
58
|
+
return candidate || `step-${index + 1}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeDependsOn(step: PlanStepInput): string[] {
|
|
62
|
+
const raw = Array.isArray(step.dependsOn)
|
|
63
|
+
? step.dependsOn
|
|
64
|
+
: (Array.isArray(step.depends_on) ? step.depends_on : []);
|
|
65
|
+
const unique = new Set<string>();
|
|
66
|
+
for (const dep of raw) {
|
|
67
|
+
const trimmed = String(dep || '').trim();
|
|
68
|
+
if (trimmed) unique.add(trimmed);
|
|
69
|
+
}
|
|
70
|
+
return Array.from(unique);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatStepLine(step: AnalyzedPlanStep): string {
|
|
74
|
+
return `- [${step.ref}] ${step.title}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatWaitReason(step: AnalyzedPlanStep): string {
|
|
78
|
+
const blockers = [
|
|
79
|
+
...step.incompleteDependencies,
|
|
80
|
+
...step.unresolvedDependencies.map(dep => `${dep} (missing)`),
|
|
81
|
+
];
|
|
82
|
+
if (blockers.length === 0) return 'waiting';
|
|
83
|
+
return `waiting on ${blockers.join(', ')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stepDependencyBlockers(step: AnalyzedPlanStep): string[] {
|
|
87
|
+
return [
|
|
88
|
+
...step.incompleteDependencies,
|
|
89
|
+
...step.unresolvedDependencies.map(dep => `${dep} (missing)`),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildPlanExecutionSnapshot(steps: PlanStepInput[]): PlanExecutionSnapshot {
|
|
94
|
+
const normalized: AnalyzedPlanStep[] = steps.map((step, index) => {
|
|
95
|
+
const status = normalizeStatus(step.status);
|
|
96
|
+
return {
|
|
97
|
+
ref: normalizeStepRef(step, index),
|
|
98
|
+
title: (step.title || '(untitled)').trim() || '(untitled)',
|
|
99
|
+
description: (step.description || '').trim(),
|
|
100
|
+
status,
|
|
101
|
+
blockedReason: (step.blockedReason || step.blocked_reason || '').trim(),
|
|
102
|
+
dependsOn: normalizeDependsOn(step),
|
|
103
|
+
unresolvedDependencies: [],
|
|
104
|
+
incompleteDependencies: [],
|
|
105
|
+
isDone: DONE_STATUSES.has(status),
|
|
106
|
+
isBlocked: BLOCKED_STATUSES.has(status),
|
|
107
|
+
original: step,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const stepByRef: Record<string, AnalyzedPlanStep> = {};
|
|
112
|
+
for (const step of normalized) {
|
|
113
|
+
if (!stepByRef[step.ref]) stepByRef[step.ref] = step;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const completedRefs = new Set(normalized.filter(step => step.isDone).map(step => step.ref));
|
|
117
|
+
|
|
118
|
+
for (const step of normalized) {
|
|
119
|
+
if (step.isDone || step.isBlocked) continue;
|
|
120
|
+
for (const dep of step.dependsOn) {
|
|
121
|
+
if (!stepByRef[dep]) {
|
|
122
|
+
step.unresolvedDependencies.push(dep);
|
|
123
|
+
} else if (!completedRefs.has(dep)) {
|
|
124
|
+
step.incompleteDependencies.push(dep);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const readySteps: AnalyzedPlanStep[] = [];
|
|
130
|
+
const waitingSteps: AnalyzedPlanStep[] = [];
|
|
131
|
+
const blockedStepDetails: AnalyzedPlanStep[] = [];
|
|
132
|
+
|
|
133
|
+
for (const step of normalized) {
|
|
134
|
+
if (step.isDone) continue;
|
|
135
|
+
if (step.isBlocked) {
|
|
136
|
+
blockedStepDetails.push(step);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (step.incompleteDependencies.length === 0 && step.unresolvedDependencies.length === 0) {
|
|
140
|
+
readySteps.push(step);
|
|
141
|
+
} else {
|
|
142
|
+
waitingSteps.push(step);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const totalSteps = normalized.length;
|
|
147
|
+
const completedSteps = normalized.filter(step => step.isDone).length;
|
|
148
|
+
const blockedSteps = blockedStepDetails.length;
|
|
149
|
+
const completionPct = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
totalSteps,
|
|
153
|
+
completedSteps,
|
|
154
|
+
blockedSteps,
|
|
155
|
+
waitingSteps,
|
|
156
|
+
readySteps,
|
|
157
|
+
blockedStepDetails,
|
|
158
|
+
allSteps: normalized,
|
|
159
|
+
stepByRef,
|
|
160
|
+
completionPct,
|
|
161
|
+
nextReadyStep: readySteps[0] || null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildPlanAgentBrief(plan: PlanInput, snapshot: PlanExecutionSnapshot): string {
|
|
166
|
+
const title = (plan.title || plan.id || '(untitled)').trim();
|
|
167
|
+
const lines: string[] = [
|
|
168
|
+
'# Plan execution brief',
|
|
169
|
+
`Plan: ${title}`,
|
|
170
|
+
];
|
|
171
|
+
if (plan.id) lines.push(`Plan ID: ${plan.id}`);
|
|
172
|
+
if (plan.status) lines.push(`Status: ${plan.status}`);
|
|
173
|
+
if (plan.scope) lines.push(`Scope: ${plan.scope}`);
|
|
174
|
+
if (plan.description) lines.push('', 'Description:', plan.description.trim());
|
|
175
|
+
|
|
176
|
+
lines.push(
|
|
177
|
+
'',
|
|
178
|
+
'Execution summary:',
|
|
179
|
+
`- Completed: ${snapshot.completedSteps}/${snapshot.totalSteps} (${snapshot.completionPct}%)`,
|
|
180
|
+
`- Ready now: ${snapshot.readySteps.length}`,
|
|
181
|
+
`- Waiting on dependencies: ${snapshot.waitingSteps.length}`,
|
|
182
|
+
`- Explicitly blocked: ${snapshot.blockedStepDetails.length}`
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (snapshot.readySteps.length > 0) {
|
|
186
|
+
lines.push('', `Ready now (${snapshot.readySteps.length}):`);
|
|
187
|
+
for (const step of snapshot.readySteps) lines.push(formatStepLine(step));
|
|
188
|
+
} else {
|
|
189
|
+
lines.push('', 'Ready now: none');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (snapshot.waitingSteps.length > 0) {
|
|
193
|
+
lines.push('', `Waiting (${snapshot.waitingSteps.length}):`);
|
|
194
|
+
for (const step of snapshot.waitingSteps) {
|
|
195
|
+
lines.push(`${formatStepLine(step)} (${formatWaitReason(step)})`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (snapshot.blockedStepDetails.length > 0) {
|
|
200
|
+
lines.push('', `Blocked (${snapshot.blockedStepDetails.length}):`);
|
|
201
|
+
for (const step of snapshot.blockedStepDetails) {
|
|
202
|
+
const reason = step.blockedReason ? `: ${step.blockedReason}` : '';
|
|
203
|
+
lines.push(`${formatStepLine(step)}${reason}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
lines.push(
|
|
208
|
+
'',
|
|
209
|
+
'Instruction:',
|
|
210
|
+
'Pick the top ready step, execute it end-to-end, and report tests/evidence before moving to the next.'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function buildNextStepPrompt(
|
|
217
|
+
plan: PlanInput,
|
|
218
|
+
snapshot: PlanExecutionSnapshot,
|
|
219
|
+
stepRef?: string
|
|
220
|
+
): string {
|
|
221
|
+
const step = stepRef ? snapshot.stepByRef[stepRef] : snapshot.nextReadyStep;
|
|
222
|
+
if (!step) {
|
|
223
|
+
const planName = (plan.title || plan.id || '(untitled)').trim();
|
|
224
|
+
return [
|
|
225
|
+
`No ready step is currently available for plan "${planName}".`,
|
|
226
|
+
'Please inspect waiting and blocked steps, resolve dependencies, and refresh the plan.',
|
|
227
|
+
].join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const planName = (plan.title || plan.id || '(untitled)').trim();
|
|
231
|
+
const dependencyBlockers = stepDependencyBlockers(step);
|
|
232
|
+
const isWaiting = dependencyBlockers.length > 0 && !step.isBlocked;
|
|
233
|
+
const readinessLabel = step.isBlocked ? 'blocked' : (isWaiting ? 'waiting' : 'ready');
|
|
234
|
+
const lines: string[] = [
|
|
235
|
+
'# Next step execution prompt',
|
|
236
|
+
`You are working on plan "${planName}"${plan.id ? ` (${plan.id})` : ''}.`,
|
|
237
|
+
`Current progress: ${snapshot.completedSteps}/${snapshot.totalSteps} complete.`,
|
|
238
|
+
`Step readiness: ${readinessLabel}.`,
|
|
239
|
+
'',
|
|
240
|
+
`Execute step [${step.ref}] ${step.title}.`,
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
if (step.description) {
|
|
244
|
+
lines.push(`Step details: ${step.description}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (step.isBlocked) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`Blocked reason: ${step.blockedReason || 'No reason provided.'}`,
|
|
250
|
+
'This step is explicitly blocked. Resolve the blocker before implementation.'
|
|
251
|
+
);
|
|
252
|
+
} else if (isWaiting) {
|
|
253
|
+
lines.push(
|
|
254
|
+
`Pending dependencies: ${dependencyBlockers.join(', ')}`,
|
|
255
|
+
'This step is waiting on dependencies. Do not start implementation until dependencies are complete.'
|
|
256
|
+
);
|
|
257
|
+
} else if (step.dependsOn.length > 0) {
|
|
258
|
+
lines.push(`Dependencies already satisfied: ${step.dependsOn.join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lines.push('', 'Execution requirements:');
|
|
262
|
+
if (step.isBlocked || isWaiting) {
|
|
263
|
+
lines.push(
|
|
264
|
+
'- Resolve blockers/dependencies first, then refresh this plan snapshot.',
|
|
265
|
+
'- Once ready, execute the step in production-ready quality.',
|
|
266
|
+
'- Report exact commands/outcomes and evidence collected.',
|
|
267
|
+
'- Update dependency and step status notes after each change.'
|
|
268
|
+
);
|
|
269
|
+
} else {
|
|
270
|
+
lines.push(
|
|
271
|
+
'- Implement in production-ready quality.',
|
|
272
|
+
'- Add or update tests where feasible.',
|
|
273
|
+
'- Report the exact commands and outcomes.',
|
|
274
|
+
'- Update the step status when done.'
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return lines.join('\n');
|
|
279
|
+
}
|
package/public/src/views/plan.js
CHANGED
|
@@ -6,8 +6,11 @@ import { api } from '../api.js';
|
|
|
6
6
|
import { escHtml } from '../utils.js';
|
|
7
7
|
import { toast } from '../components/toast.js';
|
|
8
8
|
import { showModal, hideModal, createModal, confirmDialog } from '../components/modals.js';
|
|
9
|
+
import { buildPlanExecutionSnapshot, buildPlanAgentBrief, buildNextStepPrompt } from './plan-execution.js';
|
|
9
10
|
// ─── State ───────────────────────────────────────────────────
|
|
10
11
|
let currentPlanId = null;
|
|
12
|
+
let currentPlanData = null;
|
|
13
|
+
let currentExecutionSnapshot = null;
|
|
11
14
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
12
15
|
function stepRef(step) {
|
|
13
16
|
return step.ref || step.id || '';
|
|
@@ -25,10 +28,91 @@ function stepStatusBadge(status) {
|
|
|
25
28
|
const color = colors[s] || 'var(--text-muted)';
|
|
26
29
|
return `<span style="font-size:11px;padding:2px 7px;border-radius:4px;background:color-mix(in srgb,${color} 18%,transparent);color:${color};font-weight:600;letter-spacing:.3px">${escHtml(s)}</span>`;
|
|
27
30
|
}
|
|
28
|
-
function
|
|
31
|
+
function metricBadge(label, value, color) {
|
|
32
|
+
return `
|
|
33
|
+
<div style="padding:8px 10px;border-radius:8px;background:color-mix(in srgb,${color} 14%, var(--bg-card));border:1px solid color-mix(in srgb,${color} 28%, var(--border));min-width:95px">
|
|
34
|
+
<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.4px">${escHtml(label)}</div>
|
|
35
|
+
<div style="font-size:14px;font-weight:600;color:${color};margin-top:2px">${escHtml(value)}</div>
|
|
36
|
+
</div>`;
|
|
37
|
+
}
|
|
38
|
+
function renderDependencyHint(analyzed) {
|
|
39
|
+
if (!analyzed || analyzed.dependsOn.length === 0 || analyzed.isDone || analyzed.isBlocked)
|
|
40
|
+
return '';
|
|
41
|
+
if (analyzed.incompleteDependencies.length === 0 && analyzed.unresolvedDependencies.length === 0) {
|
|
42
|
+
return `<div style="font-size:12px;color:var(--text-muted);margin-top:3px">Dependencies complete: ${escHtml(analyzed.dependsOn.join(', '))}</div>`;
|
|
43
|
+
}
|
|
44
|
+
const blockers = [
|
|
45
|
+
...analyzed.incompleteDependencies,
|
|
46
|
+
...analyzed.unresolvedDependencies.map(dep => `${dep} (missing)`),
|
|
47
|
+
];
|
|
48
|
+
return `<div style="font-size:12px;color:var(--warning,#f59e0b);margin-top:3px">Waiting on: ${escHtml(blockers.join(', '))}</div>`;
|
|
49
|
+
}
|
|
50
|
+
function renderExecutionFocus(planId, snapshot) {
|
|
51
|
+
const next = snapshot.nextReadyStep;
|
|
52
|
+
const waitingPreview = snapshot.waitingSteps.slice(0, 2).map(step => {
|
|
53
|
+
const blockers = [
|
|
54
|
+
...step.incompleteDependencies,
|
|
55
|
+
...step.unresolvedDependencies.map(dep => `${dep} (missing)`),
|
|
56
|
+
];
|
|
57
|
+
return `
|
|
58
|
+
<li style="font-size:12px;color:var(--text-secondary);line-height:1.5">
|
|
59
|
+
<span style="font-family:'JetBrains Mono',monospace;color:var(--text-muted)">[${escHtml(step.ref)}]</span>
|
|
60
|
+
${escHtml(step.title)} - waiting on ${escHtml(blockers.join(', '))}
|
|
61
|
+
</li>`;
|
|
62
|
+
}).join('');
|
|
63
|
+
const blockedPreview = snapshot.blockedStepDetails.slice(0, 2).map(step => `
|
|
64
|
+
<li style="font-size:12px;color:var(--text-secondary);line-height:1.5">
|
|
65
|
+
<span style="font-family:'JetBrains Mono',monospace;color:var(--text-muted)">[${escHtml(step.ref)}]</span>
|
|
66
|
+
${escHtml(step.title)}
|
|
67
|
+
${step.blockedReason ? `<span style="color:var(--status-blocked)"> - ${escHtml(step.blockedReason)}</span>` : ''}
|
|
68
|
+
</li>`).join('');
|
|
69
|
+
return `
|
|
70
|
+
<div style="margin-bottom:12px;padding:12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated)">
|
|
71
|
+
<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:flex-start">
|
|
72
|
+
<div>
|
|
73
|
+
<div style="font-size:13px;font-weight:600">Execution Focus</div>
|
|
74
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:2px">Dependency-aware summary to guide agent execution.</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
77
|
+
<button class="btn btn-ghost btn-sm" onclick="window.__app.copyPlanAgentBrief('${escHtml(planId)}')" title="Copy dependency and status summary">Copy agent brief</button>
|
|
78
|
+
${next ? `<button class="btn btn-secondary btn-sm" onclick="window.__app.copyPlanNextStepPrompt('${escHtml(planId)}','${escHtml(next.ref)}')" title="Copy prompt for the next ready step">Copy next-step prompt</button>` : ''}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
82
|
+
${metricBadge('Complete', `${snapshot.completedSteps}/${snapshot.totalSteps}`, 'var(--status-closed)')}
|
|
83
|
+
${metricBadge('Ready', String(snapshot.readySteps.length), 'var(--accent)')}
|
|
84
|
+
${metricBadge('Waiting', String(snapshot.waitingSteps.length), 'var(--warning,#f59e0b)')}
|
|
85
|
+
${metricBadge('Blocked', String(snapshot.blockedStepDetails.length), 'var(--status-blocked)')}
|
|
86
|
+
</div>
|
|
87
|
+
${next
|
|
88
|
+
? `<div style="margin-top:10px;font-size:12px;color:var(--text-secondary)">Next ready step: <span style="font-family:'JetBrains Mono',monospace;color:var(--text-muted)">[${escHtml(next.ref)}]</span> <strong>${escHtml(next.title)}</strong></div>`
|
|
89
|
+
: '<div style="margin-top:10px;font-size:12px;color:var(--text-muted)">No ready steps right now. Resolve blockers or dependencies to continue.</div>'}
|
|
90
|
+
${waitingPreview
|
|
91
|
+
? `<div style="margin-top:8px">
|
|
92
|
+
<div style="font-size:12px;font-weight:600;color:var(--text-secondary)">Waiting queue</div>
|
|
93
|
+
<ul style="margin:6px 0 0 16px;padding:0;display:flex;flex-direction:column;gap:4px">${waitingPreview}</ul>
|
|
94
|
+
${snapshot.waitingSteps.length > 2 ? `<div style="font-size:11px;color:var(--text-muted);margin-top:4px">+ ${snapshot.waitingSteps.length - 2} more waiting step(s)</div>` : ''}
|
|
95
|
+
</div>`
|
|
96
|
+
: ''}
|
|
97
|
+
${blockedPreview
|
|
98
|
+
? `<div style="margin-top:8px">
|
|
99
|
+
<div style="font-size:12px;font-weight:600;color:var(--status-blocked)">Blocked steps</div>
|
|
100
|
+
<ul style="margin:6px 0 0 16px;padding:0;display:flex;flex-direction:column;gap:4px">${blockedPreview}</ul>
|
|
101
|
+
${snapshot.blockedStepDetails.length > 2 ? `<div style="font-size:11px;color:var(--text-muted);margin-top:4px">+ ${snapshot.blockedStepDetails.length - 2} more blocked step(s)</div>` : ''}
|
|
102
|
+
</div>`
|
|
103
|
+
: ''}
|
|
104
|
+
</div>`;
|
|
105
|
+
}
|
|
106
|
+
function renderStepRow(step, planId, analyzed) {
|
|
29
107
|
const ref = stepRef(step);
|
|
30
108
|
const isDone = ['done', 'completed'].includes((step.status || '').toLowerCase());
|
|
31
109
|
const isBlocked = (step.status || '').toLowerCase() === 'blocked';
|
|
110
|
+
const isWaiting = !!analyzed && !analyzed.isDone && !analyzed.isBlocked
|
|
111
|
+
&& (analyzed.incompleteDependencies.length > 0 || analyzed.unresolvedDependencies.length > 0);
|
|
112
|
+
const promptTitle = isBlocked
|
|
113
|
+
? 'Copy blocker-resolution prompt'
|
|
114
|
+
: (isWaiting ? 'Copy dependency-resolution prompt' : 'Copy execution prompt');
|
|
115
|
+
const dependencyHint = renderDependencyHint(analyzed);
|
|
32
116
|
return `
|
|
33
117
|
<div class="plan-step-row" data-step-ref="${escHtml(ref)}">
|
|
34
118
|
<div style="flex:1;min-width:0">
|
|
@@ -38,9 +122,11 @@ function renderStepRow(step, planId) {
|
|
|
38
122
|
${stepStatusBadge(step.status)}
|
|
39
123
|
</div>
|
|
40
124
|
${step.description ? `<div style="font-size:12px;color:var(--text-muted);margin-top:3px">${escHtml(step.description)}</div>` : ''}
|
|
125
|
+
${dependencyHint}
|
|
41
126
|
${isBlocked && (step.blockedReason || step.blocked_reason) ? `<div style="font-size:12px;color:var(--status-blocked);margin-top:3px">Blocked: ${escHtml(step.blockedReason || step.blocked_reason || '')}</div>` : ''}
|
|
42
127
|
</div>
|
|
43
128
|
<div style="display:flex;gap:6px;flex-shrink:0">
|
|
129
|
+
${!isDone ? `<button class="btn btn-ghost btn-sm" onclick="window.__app.copyPlanNextStepPrompt('${escHtml(planId)}','${escHtml(ref)}')" title="${escHtml(promptTitle)}" ${isBlocked ? 'style="color:var(--status-blocked)"' : ''}>⧉</button>` : ''}
|
|
44
130
|
${!isDone ? `<button class="btn btn-ghost btn-sm" onclick="window.__app.planCompleteStep('${escHtml(planId)}','${escHtml(ref)}')" title="Mark complete">✓</button>` : ''}
|
|
45
131
|
${!isBlocked && !isDone ? `<button class="btn btn-ghost btn-sm" onclick="window.__app.planBlockStepPrompt('${escHtml(planId)}','${escHtml(ref)}')" title="Block">⊘</button>` : ''}
|
|
46
132
|
<button class="btn btn-ghost btn-sm" onclick="window.__app.planRemoveStep('${escHtml(planId)}','${escHtml(ref)}')" title="Remove" style="color:var(--danger,#f87171)">✕</button>
|
|
@@ -75,6 +161,9 @@ export async function initPlanView() {
|
|
|
75
161
|
const el = document.getElementById('content-plan');
|
|
76
162
|
if (!el)
|
|
77
163
|
return;
|
|
164
|
+
currentPlanId = null;
|
|
165
|
+
currentPlanData = null;
|
|
166
|
+
currentExecutionSnapshot = null;
|
|
78
167
|
if (!state.currentProject) {
|
|
79
168
|
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
80
169
|
return;
|
|
@@ -95,6 +184,9 @@ async function loadPlanList() {
|
|
|
95
184
|
if (subEl)
|
|
96
185
|
subEl.textContent = state.currentProject.name;
|
|
97
186
|
if (items.length === 0) {
|
|
187
|
+
currentPlanId = null;
|
|
188
|
+
currentPlanData = null;
|
|
189
|
+
currentExecutionSnapshot = null;
|
|
98
190
|
listEl.innerHTML = '<div class="empty-state" style="padding:16px"><div class="empty-state-text">No plans yet</div></div>';
|
|
99
191
|
return;
|
|
100
192
|
}
|
|
@@ -130,37 +222,127 @@ export async function openPlanDetail(planId) {
|
|
|
130
222
|
const data = await api('GET', `/projects/${state.currentProject.id}/pm/plan/${encodeURIComponent(planId)}`);
|
|
131
223
|
const plan = (data.plan || data);
|
|
132
224
|
const steps = plan.steps || [];
|
|
225
|
+
const snapshot = buildPlanExecutionSnapshot(steps);
|
|
226
|
+
currentPlanData = plan;
|
|
227
|
+
currentExecutionSnapshot = snapshot;
|
|
133
228
|
const isApproved = !!(plan.approvedAt || plan.approved_at);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
229
|
+
const card = document.createElement('div');
|
|
230
|
+
card.className = 'card';
|
|
231
|
+
const header = document.createElement('div');
|
|
232
|
+
header.className = 'card-header';
|
|
233
|
+
header.style.display = 'flex';
|
|
234
|
+
header.style.alignItems = 'center';
|
|
235
|
+
header.style.justifyContent = 'space-between';
|
|
236
|
+
header.style.flexWrap = 'wrap';
|
|
237
|
+
header.style.gap = '8px';
|
|
238
|
+
const heading = document.createElement('div');
|
|
239
|
+
const titleEl = document.createElement('div');
|
|
240
|
+
titleEl.className = 'card-title';
|
|
241
|
+
titleEl.textContent = plan.title || planId;
|
|
242
|
+
const idEl = document.createElement('div');
|
|
243
|
+
idEl.style.fontSize = '12px';
|
|
244
|
+
idEl.style.color = 'var(--text-muted)';
|
|
245
|
+
idEl.style.marginTop = '2px';
|
|
246
|
+
idEl.style.fontFamily = "'JetBrains Mono',monospace";
|
|
247
|
+
idEl.textContent = plan.id || planId;
|
|
248
|
+
heading.append(titleEl, idEl);
|
|
249
|
+
const actions = document.createElement('div');
|
|
250
|
+
actions.style.display = 'flex';
|
|
251
|
+
actions.style.gap = '8px';
|
|
252
|
+
actions.style.flexWrap = 'wrap';
|
|
253
|
+
if (!isApproved) {
|
|
254
|
+
const approveBtn = document.createElement('button');
|
|
255
|
+
approveBtn.className = 'btn btn-secondary btn-sm';
|
|
256
|
+
approveBtn.textContent = '✓ Approve';
|
|
257
|
+
approveBtn.addEventListener('click', () => { void planApprove(planId); });
|
|
258
|
+
actions.appendChild(approveBtn);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const approvedLabel = document.createElement('span');
|
|
262
|
+
approvedLabel.style.fontSize = '12px';
|
|
263
|
+
approvedLabel.style.color = 'var(--status-closed)';
|
|
264
|
+
approvedLabel.textContent = '✓ Approved';
|
|
265
|
+
actions.appendChild(approvedLabel);
|
|
266
|
+
}
|
|
267
|
+
const materializeBtn = document.createElement('button');
|
|
268
|
+
materializeBtn.className = 'btn btn-primary btn-sm';
|
|
269
|
+
materializeBtn.textContent = '⇗ Materialize';
|
|
270
|
+
materializeBtn.addEventListener('click', () => planMaterializePrompt(planId));
|
|
271
|
+
actions.appendChild(materializeBtn);
|
|
272
|
+
const editBtn = document.createElement('button');
|
|
273
|
+
editBtn.className = 'btn btn-ghost btn-sm';
|
|
274
|
+
editBtn.title = 'Edit plan';
|
|
275
|
+
editBtn.textContent = '✎';
|
|
276
|
+
editBtn.addEventListener('click', () => planEditPrompt(planId, plan.title || ''));
|
|
277
|
+
actions.appendChild(editBtn);
|
|
278
|
+
const deleteBtn = document.createElement('button');
|
|
279
|
+
deleteBtn.className = 'btn btn-ghost btn-sm';
|
|
280
|
+
deleteBtn.title = 'Delete plan';
|
|
281
|
+
deleteBtn.style.color = 'var(--danger,#f87171)';
|
|
282
|
+
deleteBtn.textContent = '✕';
|
|
283
|
+
deleteBtn.addEventListener('click', () => planDeletePrompt(planId));
|
|
284
|
+
actions.appendChild(deleteBtn);
|
|
285
|
+
header.append(heading, actions);
|
|
286
|
+
const body = document.createElement('div');
|
|
287
|
+
body.className = 'card-body';
|
|
288
|
+
if (plan.description) {
|
|
289
|
+
const descEl = document.createElement('div');
|
|
290
|
+
descEl.style.fontSize = '13px';
|
|
291
|
+
descEl.style.color = 'var(--text-secondary)';
|
|
292
|
+
descEl.style.marginBottom = '12px';
|
|
293
|
+
descEl.textContent = plan.description;
|
|
294
|
+
body.appendChild(descEl);
|
|
295
|
+
}
|
|
296
|
+
if (plan.scope) {
|
|
297
|
+
const scopeEl = document.createElement('div');
|
|
298
|
+
scopeEl.style.fontSize = '12px';
|
|
299
|
+
scopeEl.style.color = 'var(--text-muted)';
|
|
300
|
+
scopeEl.style.marginBottom = '8px';
|
|
301
|
+
scopeEl.textContent = `Scope: ${plan.scope}`;
|
|
302
|
+
body.appendChild(scopeEl);
|
|
303
|
+
}
|
|
304
|
+
const focusTemplate = document.createElement('template');
|
|
305
|
+
focusTemplate.innerHTML = renderExecutionFocus(planId, snapshot);
|
|
306
|
+
body.appendChild(focusTemplate.content);
|
|
307
|
+
const stepsHeader = document.createElement('div');
|
|
308
|
+
stepsHeader.style.display = 'flex';
|
|
309
|
+
stepsHeader.style.alignItems = 'center';
|
|
310
|
+
stepsHeader.style.justifyContent = 'space-between';
|
|
311
|
+
stepsHeader.style.marginBottom = '8px';
|
|
312
|
+
const stepsTitle = document.createElement('div');
|
|
313
|
+
stepsTitle.style.fontSize = '13px';
|
|
314
|
+
stepsTitle.style.fontWeight = '600';
|
|
315
|
+
stepsTitle.textContent = `Steps (${steps.length})`;
|
|
316
|
+
const addStepBtn = document.createElement('button');
|
|
317
|
+
addStepBtn.className = 'btn btn-ghost btn-sm';
|
|
318
|
+
addStepBtn.textContent = '+ Add Step';
|
|
319
|
+
addStepBtn.addEventListener('click', () => openAddStepModal(planId));
|
|
320
|
+
stepsHeader.append(stepsTitle, addStepBtn);
|
|
321
|
+
body.appendChild(stepsHeader);
|
|
322
|
+
const stepsList = document.createElement('div');
|
|
323
|
+
stepsList.id = 'plan-steps-list';
|
|
324
|
+
if (steps.length === 0) {
|
|
325
|
+
const emptySteps = document.createElement('div');
|
|
326
|
+
emptySteps.style.fontSize = '13px';
|
|
327
|
+
emptySteps.style.color = 'var(--text-muted)';
|
|
328
|
+
emptySteps.style.padding = '8px 0';
|
|
329
|
+
emptySteps.textContent = 'No steps yet. Add the first step to get started.';
|
|
330
|
+
stepsList.appendChild(emptySteps);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
steps.forEach((step, index) => {
|
|
334
|
+
const rowTemplate = document.createElement('template');
|
|
335
|
+
rowTemplate.innerHTML = renderStepRow(step, planId, snapshot.allSteps[index]);
|
|
336
|
+
stepsList.appendChild(rowTemplate.content);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
body.appendChild(stepsList);
|
|
340
|
+
card.append(header, body);
|
|
341
|
+
detailEl.replaceChildren(card);
|
|
162
342
|
}
|
|
163
343
|
catch (err) {
|
|
344
|
+
currentPlanData = null;
|
|
345
|
+
currentExecutionSnapshot = null;
|
|
164
346
|
detailEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
165
347
|
}
|
|
166
348
|
}
|
|
@@ -391,6 +573,60 @@ export async function submitMaterializePlan(planId) {
|
|
|
391
573
|
toast(err instanceof Error ? err.message : 'Failed to materialize plan', 'error');
|
|
392
574
|
}
|
|
393
575
|
}
|
|
576
|
+
async function copyTextWithFallback(modalTitle, text, successMessage) {
|
|
577
|
+
if (navigator.clipboard?.writeText) {
|
|
578
|
+
try {
|
|
579
|
+
await navigator.clipboard.writeText(text);
|
|
580
|
+
toast(successMessage, 'success');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// Fall through to manual-copy modal.
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const modalId = 'plan-copy-fallback-modal';
|
|
588
|
+
createModal(modalId, modalTitle, `
|
|
589
|
+
<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Clipboard access is unavailable in this browser context. Copy manually:</p>
|
|
590
|
+
<textarea class="form-textarea" id="plan-copy-fallback-text" rows="12" spellcheck="false"></textarea>`, `<button class="btn btn-primary" onclick="window.__app.hideModal('${modalId}')">Close</button>`);
|
|
591
|
+
showModal(modalId);
|
|
592
|
+
const el = document.getElementById('plan-copy-fallback-text');
|
|
593
|
+
if (el) {
|
|
594
|
+
el.value = text;
|
|
595
|
+
el.focus();
|
|
596
|
+
el.select();
|
|
597
|
+
}
|
|
598
|
+
toast('Clipboard blocked. Opened manual copy panel.', 'info');
|
|
599
|
+
}
|
|
600
|
+
function getCurrentPlanContext(planId) {
|
|
601
|
+
const activeId = currentPlanData?.id || currentPlanId;
|
|
602
|
+
if (!currentPlanData || !currentExecutionSnapshot || activeId !== planId) {
|
|
603
|
+
toast('Open this plan first to generate prompts', 'info');
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
return { plan: currentPlanData, snapshot: currentExecutionSnapshot };
|
|
607
|
+
}
|
|
608
|
+
export async function copyPlanAgentBrief(planId) {
|
|
609
|
+
const ctx = getCurrentPlanContext(planId);
|
|
610
|
+
if (!ctx)
|
|
611
|
+
return;
|
|
612
|
+
const brief = buildPlanAgentBrief(ctx.plan, ctx.snapshot);
|
|
613
|
+
await copyTextWithFallback('Plan Agent Brief', brief, 'Agent brief copied');
|
|
614
|
+
}
|
|
615
|
+
export async function copyPlanNextStepPrompt(planId, stepRef) {
|
|
616
|
+
const ctx = getCurrentPlanContext(planId);
|
|
617
|
+
if (!ctx)
|
|
618
|
+
return;
|
|
619
|
+
if (stepRef && !ctx.snapshot.stepByRef[stepRef]) {
|
|
620
|
+
toast(`Step ${stepRef} not found`, 'error');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (!stepRef && !ctx.snapshot.nextReadyStep) {
|
|
624
|
+
toast('No ready step available right now', 'info');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const prompt = buildNextStepPrompt(ctx.plan, ctx.snapshot, stepRef);
|
|
628
|
+
await copyTextWithFallback('Next Step Prompt', prompt, 'Next-step prompt copied');
|
|
629
|
+
}
|
|
394
630
|
export function planEditPrompt(planId, currentTitle) {
|
|
395
631
|
createModal('edit-plan-modal', 'Edit Plan', `
|
|
396
632
|
<div class="form-group">
|
|
@@ -439,6 +675,8 @@ export function planDeletePrompt(planId) {
|
|
|
439
675
|
await api('DELETE', `/projects/${state.currentProject.id}/pm/plan/${encodeURIComponent(planId)}`, {});
|
|
440
676
|
toast('Plan deleted', 'success');
|
|
441
677
|
currentPlanId = null;
|
|
678
|
+
currentPlanData = null;
|
|
679
|
+
currentExecutionSnapshot = null;
|
|
442
680
|
const detailEl = document.getElementById('plan-detail-panel');
|
|
443
681
|
if (detailEl)
|
|
444
682
|
detailEl.innerHTML = '<div class="empty-state"><div class="empty-state-text">Select a plan to view its steps</div></div>';
|