@unbrained/pm-web 2026.6.4 → 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 +42 -1
- package/dist/board.js +27 -7
- package/dist/board.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/routes/admin.js +11 -10
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/github.js +19 -18
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/groups.js +11 -10
- package/dist/routes/groups.js.map +1 -1
- package/dist/routes/pm.js +199 -198
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +4 -3
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/route-params.js +7 -0
- package/dist/routes/route-params.js.map +1 -0
- package/dist/routes/sharing.js +5 -4
- package/dist/routes/sharing.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 +17 -15
- 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/utils.js +3 -1
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.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
package/public/src/views/plan.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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, type AnalyzedPlanStep, type PlanExecutionSnapshot } from './plan-execution.js';
|
|
9
10
|
|
|
10
11
|
// ─── Types ───────────────────────────────────────────────────
|
|
11
12
|
|
|
@@ -39,6 +40,8 @@ type PlanData = {
|
|
|
39
40
|
// ─── State ───────────────────────────────────────────────────
|
|
40
41
|
|
|
41
42
|
let currentPlanId: string | null = null;
|
|
43
|
+
let currentPlanData: PlanData | null = null;
|
|
44
|
+
let currentExecutionSnapshot: PlanExecutionSnapshot | null = null;
|
|
42
45
|
|
|
43
46
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
44
47
|
|
|
@@ -60,10 +63,96 @@ function stepStatusBadge(status?: string): string {
|
|
|
60
63
|
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>`;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
function
|
|
66
|
+
function metricBadge(label: string, value: string, color: string): string {
|
|
67
|
+
return `
|
|
68
|
+
<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">
|
|
69
|
+
<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.4px">${escHtml(label)}</div>
|
|
70
|
+
<div style="font-size:14px;font-weight:600;color:${color};margin-top:2px">${escHtml(value)}</div>
|
|
71
|
+
</div>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderDependencyHint(analyzed?: AnalyzedPlanStep): string {
|
|
75
|
+
if (!analyzed || analyzed.dependsOn.length === 0 || analyzed.isDone || analyzed.isBlocked) return '';
|
|
76
|
+
|
|
77
|
+
if (analyzed.incompleteDependencies.length === 0 && analyzed.unresolvedDependencies.length === 0) {
|
|
78
|
+
return `<div style="font-size:12px;color:var(--text-muted);margin-top:3px">Dependencies complete: ${escHtml(analyzed.dependsOn.join(', '))}</div>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const blockers = [
|
|
82
|
+
...analyzed.incompleteDependencies,
|
|
83
|
+
...analyzed.unresolvedDependencies.map(dep => `${dep} (missing)`),
|
|
84
|
+
];
|
|
85
|
+
return `<div style="font-size:12px;color:var(--warning,#f59e0b);margin-top:3px">Waiting on: ${escHtml(blockers.join(', '))}</div>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderExecutionFocus(planId: string, snapshot: PlanExecutionSnapshot): string {
|
|
89
|
+
const next = snapshot.nextReadyStep;
|
|
90
|
+
const waitingPreview = snapshot.waitingSteps.slice(0, 2).map(step => {
|
|
91
|
+
const blockers = [
|
|
92
|
+
...step.incompleteDependencies,
|
|
93
|
+
...step.unresolvedDependencies.map(dep => `${dep} (missing)`),
|
|
94
|
+
];
|
|
95
|
+
return `
|
|
96
|
+
<li style="font-size:12px;color:var(--text-secondary);line-height:1.5">
|
|
97
|
+
<span style="font-family:'JetBrains Mono',monospace;color:var(--text-muted)">[${escHtml(step.ref)}]</span>
|
|
98
|
+
${escHtml(step.title)} - waiting on ${escHtml(blockers.join(', '))}
|
|
99
|
+
</li>`;
|
|
100
|
+
}).join('');
|
|
101
|
+
const blockedPreview = snapshot.blockedStepDetails.slice(0, 2).map(step => `
|
|
102
|
+
<li style="font-size:12px;color:var(--text-secondary);line-height:1.5">
|
|
103
|
+
<span style="font-family:'JetBrains Mono',monospace;color:var(--text-muted)">[${escHtml(step.ref)}]</span>
|
|
104
|
+
${escHtml(step.title)}
|
|
105
|
+
${step.blockedReason ? `<span style="color:var(--status-blocked)"> - ${escHtml(step.blockedReason)}</span>` : ''}
|
|
106
|
+
</li>`).join('');
|
|
107
|
+
|
|
108
|
+
return `
|
|
109
|
+
<div style="margin-bottom:12px;padding:12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-elevated)">
|
|
110
|
+
<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:flex-start">
|
|
111
|
+
<div>
|
|
112
|
+
<div style="font-size:13px;font-weight:600">Execution Focus</div>
|
|
113
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:2px">Dependency-aware summary to guide agent execution.</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
116
|
+
<button class="btn btn-ghost btn-sm" onclick="window.__app.copyPlanAgentBrief('${escHtml(planId)}')" title="Copy dependency and status summary">Copy agent brief</button>
|
|
117
|
+
${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>` : ''}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
121
|
+
${metricBadge('Complete', `${snapshot.completedSteps}/${snapshot.totalSteps}`, 'var(--status-closed)')}
|
|
122
|
+
${metricBadge('Ready', String(snapshot.readySteps.length), 'var(--accent)')}
|
|
123
|
+
${metricBadge('Waiting', String(snapshot.waitingSteps.length), 'var(--warning,#f59e0b)')}
|
|
124
|
+
${metricBadge('Blocked', String(snapshot.blockedStepDetails.length), 'var(--status-blocked)')}
|
|
125
|
+
</div>
|
|
126
|
+
${next
|
|
127
|
+
? `<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>`
|
|
128
|
+
: '<div style="margin-top:10px;font-size:12px;color:var(--text-muted)">No ready steps right now. Resolve blockers or dependencies to continue.</div>'}
|
|
129
|
+
${waitingPreview
|
|
130
|
+
? `<div style="margin-top:8px">
|
|
131
|
+
<div style="font-size:12px;font-weight:600;color:var(--text-secondary)">Waiting queue</div>
|
|
132
|
+
<ul style="margin:6px 0 0 16px;padding:0;display:flex;flex-direction:column;gap:4px">${waitingPreview}</ul>
|
|
133
|
+
${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>` : ''}
|
|
134
|
+
</div>`
|
|
135
|
+
: ''}
|
|
136
|
+
${blockedPreview
|
|
137
|
+
? `<div style="margin-top:8px">
|
|
138
|
+
<div style="font-size:12px;font-weight:600;color:var(--status-blocked)">Blocked steps</div>
|
|
139
|
+
<ul style="margin:6px 0 0 16px;padding:0;display:flex;flex-direction:column;gap:4px">${blockedPreview}</ul>
|
|
140
|
+
${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>` : ''}
|
|
141
|
+
</div>`
|
|
142
|
+
: ''}
|
|
143
|
+
</div>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderStepRow(step: PlanStep, planId: string, analyzed?: AnalyzedPlanStep): string {
|
|
64
147
|
const ref = stepRef(step);
|
|
65
148
|
const isDone = ['done', 'completed'].includes((step.status || '').toLowerCase());
|
|
66
149
|
const isBlocked = (step.status || '').toLowerCase() === 'blocked';
|
|
150
|
+
const isWaiting = !!analyzed && !analyzed.isDone && !analyzed.isBlocked
|
|
151
|
+
&& (analyzed.incompleteDependencies.length > 0 || analyzed.unresolvedDependencies.length > 0);
|
|
152
|
+
const promptTitle = isBlocked
|
|
153
|
+
? 'Copy blocker-resolution prompt'
|
|
154
|
+
: (isWaiting ? 'Copy dependency-resolution prompt' : 'Copy execution prompt');
|
|
155
|
+
const dependencyHint = renderDependencyHint(analyzed);
|
|
67
156
|
return `
|
|
68
157
|
<div class="plan-step-row" data-step-ref="${escHtml(ref)}">
|
|
69
158
|
<div style="flex:1;min-width:0">
|
|
@@ -73,9 +162,11 @@ function renderStepRow(step: PlanStep, planId: string): string {
|
|
|
73
162
|
${stepStatusBadge(step.status)}
|
|
74
163
|
</div>
|
|
75
164
|
${step.description ? `<div style="font-size:12px;color:var(--text-muted);margin-top:3px">${escHtml(step.description)}</div>` : ''}
|
|
165
|
+
${dependencyHint}
|
|
76
166
|
${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>` : ''}
|
|
77
167
|
</div>
|
|
78
168
|
<div style="display:flex;gap:6px;flex-shrink:0">
|
|
169
|
+
${!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>` : ''}
|
|
79
170
|
${!isDone ? `<button class="btn btn-ghost btn-sm" onclick="window.__app.planCompleteStep('${escHtml(planId)}','${escHtml(ref)}')" title="Mark complete">✓</button>` : ''}
|
|
80
171
|
${!isBlocked && !isDone ? `<button class="btn btn-ghost btn-sm" onclick="window.__app.planBlockStepPrompt('${escHtml(planId)}','${escHtml(ref)}')" title="Block">⊘</button>` : ''}
|
|
81
172
|
<button class="btn btn-ghost btn-sm" onclick="window.__app.planRemoveStep('${escHtml(planId)}','${escHtml(ref)}')" title="Remove" style="color:var(--danger,#f87171)">✕</button>
|
|
@@ -112,6 +203,9 @@ export function renderPlanView(): string {
|
|
|
112
203
|
export async function initPlanView(): Promise<void> {
|
|
113
204
|
const el = document.getElementById('content-plan');
|
|
114
205
|
if (!el) return;
|
|
206
|
+
currentPlanId = null;
|
|
207
|
+
currentPlanData = null;
|
|
208
|
+
currentExecutionSnapshot = null;
|
|
115
209
|
if (!state.currentProject) {
|
|
116
210
|
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
117
211
|
return;
|
|
@@ -134,6 +228,9 @@ async function loadPlanList(): Promise<void> {
|
|
|
134
228
|
if (subEl) subEl.textContent = state.currentProject.name;
|
|
135
229
|
|
|
136
230
|
if (items.length === 0) {
|
|
231
|
+
currentPlanId = null;
|
|
232
|
+
currentPlanData = null;
|
|
233
|
+
currentExecutionSnapshot = null;
|
|
137
234
|
listEl.innerHTML = '<div class="empty-state" style="padding:16px"><div class="empty-state-text">No plans yet</div></div>';
|
|
138
235
|
return;
|
|
139
236
|
}
|
|
@@ -172,38 +269,144 @@ export async function openPlanDetail(planId: string): Promise<void> {
|
|
|
172
269
|
const data = await api('GET', `/projects/${state.currentProject.id}/pm/plan/${encodeURIComponent(planId)}`) as any;
|
|
173
270
|
const plan = (data.plan || data) as PlanData;
|
|
174
271
|
const steps = plan.steps || [];
|
|
272
|
+
const snapshot = buildPlanExecutionSnapshot(steps);
|
|
273
|
+
currentPlanData = plan;
|
|
274
|
+
currentExecutionSnapshot = snapshot;
|
|
175
275
|
|
|
176
276
|
const isApproved = !!(plan.approvedAt || plan.approved_at);
|
|
177
277
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
278
|
+
const card = document.createElement('div');
|
|
279
|
+
card.className = 'card';
|
|
280
|
+
|
|
281
|
+
const header = document.createElement('div');
|
|
282
|
+
header.className = 'card-header';
|
|
283
|
+
header.style.display = 'flex';
|
|
284
|
+
header.style.alignItems = 'center';
|
|
285
|
+
header.style.justifyContent = 'space-between';
|
|
286
|
+
header.style.flexWrap = 'wrap';
|
|
287
|
+
header.style.gap = '8px';
|
|
288
|
+
|
|
289
|
+
const heading = document.createElement('div');
|
|
290
|
+
const titleEl = document.createElement('div');
|
|
291
|
+
titleEl.className = 'card-title';
|
|
292
|
+
titleEl.textContent = plan.title || planId;
|
|
293
|
+
const idEl = document.createElement('div');
|
|
294
|
+
idEl.style.fontSize = '12px';
|
|
295
|
+
idEl.style.color = 'var(--text-muted)';
|
|
296
|
+
idEl.style.marginTop = '2px';
|
|
297
|
+
idEl.style.fontFamily = "'JetBrains Mono',monospace";
|
|
298
|
+
idEl.textContent = plan.id || planId;
|
|
299
|
+
heading.append(titleEl, idEl);
|
|
300
|
+
|
|
301
|
+
const actions = document.createElement('div');
|
|
302
|
+
actions.style.display = 'flex';
|
|
303
|
+
actions.style.gap = '8px';
|
|
304
|
+
actions.style.flexWrap = 'wrap';
|
|
305
|
+
|
|
306
|
+
if (!isApproved) {
|
|
307
|
+
const approveBtn = document.createElement('button');
|
|
308
|
+
approveBtn.className = 'btn btn-secondary btn-sm';
|
|
309
|
+
approveBtn.textContent = '✓ Approve';
|
|
310
|
+
approveBtn.addEventListener('click', () => { void planApprove(planId); });
|
|
311
|
+
actions.appendChild(approveBtn);
|
|
312
|
+
} else {
|
|
313
|
+
const approvedLabel = document.createElement('span');
|
|
314
|
+
approvedLabel.style.fontSize = '12px';
|
|
315
|
+
approvedLabel.style.color = 'var(--status-closed)';
|
|
316
|
+
approvedLabel.textContent = '✓ Approved';
|
|
317
|
+
actions.appendChild(approvedLabel);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const materializeBtn = document.createElement('button');
|
|
321
|
+
materializeBtn.className = 'btn btn-primary btn-sm';
|
|
322
|
+
materializeBtn.textContent = '⇗ Materialize';
|
|
323
|
+
materializeBtn.addEventListener('click', () => planMaterializePrompt(planId));
|
|
324
|
+
actions.appendChild(materializeBtn);
|
|
325
|
+
|
|
326
|
+
const editBtn = document.createElement('button');
|
|
327
|
+
editBtn.className = 'btn btn-ghost btn-sm';
|
|
328
|
+
editBtn.title = 'Edit plan';
|
|
329
|
+
editBtn.textContent = '✎';
|
|
330
|
+
editBtn.addEventListener('click', () => planEditPrompt(planId, plan.title || ''));
|
|
331
|
+
actions.appendChild(editBtn);
|
|
332
|
+
|
|
333
|
+
const deleteBtn = document.createElement('button');
|
|
334
|
+
deleteBtn.className = 'btn btn-ghost btn-sm';
|
|
335
|
+
deleteBtn.title = 'Delete plan';
|
|
336
|
+
deleteBtn.style.color = 'var(--danger,#f87171)';
|
|
337
|
+
deleteBtn.textContent = '✕';
|
|
338
|
+
deleteBtn.addEventListener('click', () => planDeletePrompt(planId));
|
|
339
|
+
actions.appendChild(deleteBtn);
|
|
340
|
+
|
|
341
|
+
header.append(heading, actions);
|
|
342
|
+
|
|
343
|
+
const body = document.createElement('div');
|
|
344
|
+
body.className = 'card-body';
|
|
345
|
+
|
|
346
|
+
if (plan.description) {
|
|
347
|
+
const descEl = document.createElement('div');
|
|
348
|
+
descEl.style.fontSize = '13px';
|
|
349
|
+
descEl.style.color = 'var(--text-secondary)';
|
|
350
|
+
descEl.style.marginBottom = '12px';
|
|
351
|
+
descEl.textContent = plan.description;
|
|
352
|
+
body.appendChild(descEl);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (plan.scope) {
|
|
356
|
+
const scopeEl = document.createElement('div');
|
|
357
|
+
scopeEl.style.fontSize = '12px';
|
|
358
|
+
scopeEl.style.color = 'var(--text-muted)';
|
|
359
|
+
scopeEl.style.marginBottom = '8px';
|
|
360
|
+
scopeEl.textContent = `Scope: ${plan.scope}`;
|
|
361
|
+
body.appendChild(scopeEl);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const focusTemplate = document.createElement('template');
|
|
365
|
+
focusTemplate.innerHTML = renderExecutionFocus(planId, snapshot);
|
|
366
|
+
body.appendChild(focusTemplate.content);
|
|
367
|
+
|
|
368
|
+
const stepsHeader = document.createElement('div');
|
|
369
|
+
stepsHeader.style.display = 'flex';
|
|
370
|
+
stepsHeader.style.alignItems = 'center';
|
|
371
|
+
stepsHeader.style.justifyContent = 'space-between';
|
|
372
|
+
stepsHeader.style.marginBottom = '8px';
|
|
373
|
+
|
|
374
|
+
const stepsTitle = document.createElement('div');
|
|
375
|
+
stepsTitle.style.fontSize = '13px';
|
|
376
|
+
stepsTitle.style.fontWeight = '600';
|
|
377
|
+
stepsTitle.textContent = `Steps (${steps.length})`;
|
|
378
|
+
|
|
379
|
+
const addStepBtn = document.createElement('button');
|
|
380
|
+
addStepBtn.className = 'btn btn-ghost btn-sm';
|
|
381
|
+
addStepBtn.textContent = '+ Add Step';
|
|
382
|
+
addStepBtn.addEventListener('click', () => openAddStepModal(planId));
|
|
383
|
+
|
|
384
|
+
stepsHeader.append(stepsTitle, addStepBtn);
|
|
385
|
+
body.appendChild(stepsHeader);
|
|
386
|
+
|
|
387
|
+
const stepsList = document.createElement('div');
|
|
388
|
+
stepsList.id = 'plan-steps-list';
|
|
389
|
+
if (steps.length === 0) {
|
|
390
|
+
const emptySteps = document.createElement('div');
|
|
391
|
+
emptySteps.style.fontSize = '13px';
|
|
392
|
+
emptySteps.style.color = 'var(--text-muted)';
|
|
393
|
+
emptySteps.style.padding = '8px 0';
|
|
394
|
+
emptySteps.textContent = 'No steps yet. Add the first step to get started.';
|
|
395
|
+
stepsList.appendChild(emptySteps);
|
|
396
|
+
} else {
|
|
397
|
+
steps.forEach((step, index) => {
|
|
398
|
+
const rowTemplate = document.createElement('template');
|
|
399
|
+
rowTemplate.innerHTML = renderStepRow(step, planId, snapshot.allSteps[index]);
|
|
400
|
+
stepsList.appendChild(rowTemplate.content);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
body.appendChild(stepsList);
|
|
404
|
+
|
|
405
|
+
card.append(header, body);
|
|
406
|
+
detailEl.replaceChildren(card);
|
|
206
407
|
} catch(err: unknown) {
|
|
408
|
+
currentPlanData = null;
|
|
409
|
+
currentExecutionSnapshot = null;
|
|
207
410
|
detailEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
208
411
|
}
|
|
209
412
|
}
|
|
@@ -434,6 +637,65 @@ export async function submitMaterializePlan(planId: string): Promise<void> {
|
|
|
434
637
|
}
|
|
435
638
|
}
|
|
436
639
|
|
|
640
|
+
async function copyTextWithFallback(modalTitle: string, text: string, successMessage: string): Promise<void> {
|
|
641
|
+
if (navigator.clipboard?.writeText) {
|
|
642
|
+
try {
|
|
643
|
+
await navigator.clipboard.writeText(text);
|
|
644
|
+
toast(successMessage, 'success');
|
|
645
|
+
return;
|
|
646
|
+
} catch {
|
|
647
|
+
// Fall through to manual-copy modal.
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const modalId = 'plan-copy-fallback-modal';
|
|
652
|
+
createModal(modalId, modalTitle, `
|
|
653
|
+
<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Clipboard access is unavailable in this browser context. Copy manually:</p>
|
|
654
|
+
<textarea class="form-textarea" id="plan-copy-fallback-text" rows="12" spellcheck="false"></textarea>`,
|
|
655
|
+
`<button class="btn btn-primary" onclick="window.__app.hideModal('${modalId}')">Close</button>`);
|
|
656
|
+
showModal(modalId);
|
|
657
|
+
const el = document.getElementById('plan-copy-fallback-text') as HTMLTextAreaElement | null;
|
|
658
|
+
if (el) {
|
|
659
|
+
el.value = text;
|
|
660
|
+
el.focus();
|
|
661
|
+
el.select();
|
|
662
|
+
}
|
|
663
|
+
toast('Clipboard blocked. Opened manual copy panel.', 'info');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function getCurrentPlanContext(planId: string): { plan: PlanData; snapshot: PlanExecutionSnapshot } | null {
|
|
667
|
+
const activeId = currentPlanData?.id || currentPlanId;
|
|
668
|
+
if (!currentPlanData || !currentExecutionSnapshot || activeId !== planId) {
|
|
669
|
+
toast('Open this plan first to generate prompts', 'info');
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
return { plan: currentPlanData, snapshot: currentExecutionSnapshot };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export async function copyPlanAgentBrief(planId: string): Promise<void> {
|
|
676
|
+
const ctx = getCurrentPlanContext(planId);
|
|
677
|
+
if (!ctx) return;
|
|
678
|
+
const brief = buildPlanAgentBrief(ctx.plan, ctx.snapshot);
|
|
679
|
+
await copyTextWithFallback('Plan Agent Brief', brief, 'Agent brief copied');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export async function copyPlanNextStepPrompt(planId: string, stepRef?: string): Promise<void> {
|
|
683
|
+
const ctx = getCurrentPlanContext(planId);
|
|
684
|
+
if (!ctx) return;
|
|
685
|
+
|
|
686
|
+
if (stepRef && !ctx.snapshot.stepByRef[stepRef]) {
|
|
687
|
+
toast(`Step ${stepRef} not found`, 'error');
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (!stepRef && !ctx.snapshot.nextReadyStep) {
|
|
691
|
+
toast('No ready step available right now', 'info');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const prompt = buildNextStepPrompt(ctx.plan, ctx.snapshot, stepRef);
|
|
696
|
+
await copyTextWithFallback('Next Step Prompt', prompt, 'Next-step prompt copied');
|
|
697
|
+
}
|
|
698
|
+
|
|
437
699
|
export function planEditPrompt(planId: string, currentTitle: string): void {
|
|
438
700
|
createModal('edit-plan-modal', 'Edit Plan', `
|
|
439
701
|
<div class="form-group">
|
|
@@ -481,6 +743,8 @@ export function planDeletePrompt(planId: string): void {
|
|
|
481
743
|
await api('DELETE', `/projects/${state.currentProject.id}/pm/plan/${encodeURIComponent(planId)}`, {});
|
|
482
744
|
toast('Plan deleted', 'success');
|
|
483
745
|
currentPlanId = null;
|
|
746
|
+
currentPlanData = null;
|
|
747
|
+
currentExecutionSnapshot = null;
|
|
484
748
|
const detailEl = document.getElementById('plan-detail-panel');
|
|
485
749
|
if (detailEl) detailEl.innerHTML = '<div class="empty-state"><div class="empty-state-text">Select a plan to view its steps</div></div>';
|
|
486
750
|
await loadPlanList();
|