declare-cc 0.6.0 → 1.0.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/README.md +72 -32
- package/dist/declare-tools.cjs +2043 -543
- package/dist/public/app.js +2576 -385
- package/dist/public/index.html +984 -37
- package/package.json +6 -1
package/dist/public/app.js
CHANGED
|
@@ -87,18 +87,24 @@ let editFormError = null;
|
|
|
87
87
|
/** @type {string|null} ID of declaration showing delete confirmation */
|
|
88
88
|
let deleteConfirmId = null;
|
|
89
89
|
|
|
90
|
-
/** @type {string | null} Active derivation session ID */
|
|
90
|
+
/** @type {string | null} Active derivation session ID (legacy single-session) */
|
|
91
91
|
let derivationSessionId = null;
|
|
92
92
|
/** @type {Array<{title: string, realizes: string, reason: string}> | null} */
|
|
93
93
|
let derivationProposals = null;
|
|
94
94
|
|
|
95
|
-
/** @type {string
|
|
95
|
+
/** @type {Map<string, { sessionId: string, status: string }>} Tracks active derivations per declaration */
|
|
96
|
+
const activeDeriveMap = new Map();
|
|
97
|
+
|
|
98
|
+
/** @type {string | null} Active action derivation session ID (for the drilled-into milestone) */
|
|
96
99
|
let actionDerivationSessionId = null;
|
|
97
|
-
/** @type {string | null} Milestone ID for the current action derivation */
|
|
100
|
+
/** @type {string | null} Milestone ID for the current action derivation (drilled-into) */
|
|
98
101
|
let actionDerivationMilestoneId = null;
|
|
99
102
|
/** @type {Array<{title: string, produces: string, reason: string}> | null} */
|
|
100
103
|
let actionDerivationProposals = null;
|
|
101
104
|
|
|
105
|
+
/** @type {Set<string>} Node IDs with in-flight planning (immediate visual feedback before agent registry updates) */
|
|
106
|
+
const pendingDerivations = new Set();
|
|
107
|
+
|
|
102
108
|
/** @type {number|null} Line number currently being annotated */
|
|
103
109
|
let annotatingLine = null;
|
|
104
110
|
/** @type {string|null} Node ID of the currently displayed annotation panel */
|
|
@@ -135,6 +141,20 @@ let execCompletedActions = 0;
|
|
|
135
141
|
let execFailedActions = 0;
|
|
136
142
|
|
|
137
143
|
|
|
144
|
+
// ─── Onboarding flow state ─────────────────────────────────────────────────────
|
|
145
|
+
/** @type {'idle'|'questions'|'proposals'|'approving'} Current onboard phase */
|
|
146
|
+
let onboardPhase = 'idle';
|
|
147
|
+
/** @type {string|null} The original vision prompt */
|
|
148
|
+
let onboardPrompt = null;
|
|
149
|
+
/** @type {Array<{question: string, context: string, options?: string[]}>|null} */
|
|
150
|
+
let onboardQuestions = null;
|
|
151
|
+
/** @type {Array<{title: string, statement: string, reasoning: string, approvedId?: string}>|null} */
|
|
152
|
+
let onboardProposals = null;
|
|
153
|
+
/** @type {number} Current index being approved */
|
|
154
|
+
let onboardApproveIndex = 0;
|
|
155
|
+
/** @type {string} Streaming text buffer */
|
|
156
|
+
let onboardStreamText = '';
|
|
157
|
+
|
|
138
158
|
// ─── DOM refs ─────────────────────────────────────────────────────────────────
|
|
139
159
|
|
|
140
160
|
const $overlay = document.getElementById('overlay');
|
|
@@ -151,7 +171,7 @@ const $healthBadge = document.getElementById('health-badge');
|
|
|
151
171
|
const $lastUpdated = document.getElementById('last-updated');
|
|
152
172
|
const $refreshBtn = document.getElementById('refresh-btn');
|
|
153
173
|
const $executeMainBtn = document.getElementById('execute-main-btn');
|
|
154
|
-
|
|
174
|
+
// $activityPinned removed — replaced by agent cards panel (A-124)
|
|
155
175
|
const $statusBreadcrumb = document.getElementById('status-breadcrumb');
|
|
156
176
|
|
|
157
177
|
// Project name click → go back to declarations (home)
|
|
@@ -227,6 +247,20 @@ const $wfActionBtn = document.getElementById('wf-action-btn');
|
|
|
227
247
|
/** @type {{ state: string, nextStep: { label: string, action: string, targetId?: string }, progress: { percentage: number, declarations: number, milestones: number, actions: number, actionsDone: number } } | null} */
|
|
228
248
|
let workflowState = null;
|
|
229
249
|
|
|
250
|
+
/** @type {{ stages: Record<string, Array<{id:string,title:string,type:string,status:string,reviewState?:string,stage:string}>>, nextAction: {action:string,label:string,targetId?:string,targetType?:string}|null, progress: {total:number,done:number,percentage:number} } | null} */
|
|
251
|
+
let lifecycleData = null;
|
|
252
|
+
|
|
253
|
+
/** @type {string|null} Active lifecycle stage filter — null means show all stages */
|
|
254
|
+
let lifecycleFilter = null;
|
|
255
|
+
|
|
256
|
+
/** @type {boolean} Whether Done section is collapsed */
|
|
257
|
+
let lifecycleDoneCollapsed = true;
|
|
258
|
+
|
|
259
|
+
/** @type {string|null} Lifecycle filter for level 2 (milestones) */
|
|
260
|
+
let lifecycleFilterL2 = null;
|
|
261
|
+
/** @type {string|null} Lifecycle filter for level 3 (actions) */
|
|
262
|
+
let lifecycleFilterL3 = null;
|
|
263
|
+
|
|
230
264
|
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
231
265
|
|
|
232
266
|
/**
|
|
@@ -339,6 +373,9 @@ async function loadData() {
|
|
|
339
373
|
// Sync topbar with running operations
|
|
340
374
|
syncTopbarFromRunning();
|
|
341
375
|
|
|
376
|
+
// Restore running derivation sessions after page refresh
|
|
377
|
+
await restoreRunningDerivations();
|
|
378
|
+
|
|
342
379
|
hideOverlay();
|
|
343
380
|
renderStatusBar();
|
|
344
381
|
renderGraph();
|
|
@@ -347,6 +384,7 @@ async function loadData() {
|
|
|
347
384
|
updateLastUpdated();
|
|
348
385
|
checkProjectComplete(graph);
|
|
349
386
|
loadWorkflowState();
|
|
387
|
+
loadLifecycleData();
|
|
350
388
|
|
|
351
389
|
// Apply persisted view mode (shows correct container, hides the other)
|
|
352
390
|
switchView(viewMode);
|
|
@@ -451,6 +489,7 @@ function renderStatusBar() {
|
|
|
451
489
|
// ─── Node element builder ─────────────────────────────────────────────────────
|
|
452
490
|
|
|
453
491
|
const COMPLETED = new Set(['DONE','KEPT','HONORED']);
|
|
492
|
+
const EXECUTING_STATUSES_SET = new Set(['EXECUTING', 'IN_PROGRESS', 'RUNNING']);
|
|
454
493
|
const IN_PROGRESS_STORED = new Set(['ACTIVE']);
|
|
455
494
|
|
|
456
495
|
/**
|
|
@@ -1109,6 +1148,9 @@ function saveDrillNavState() {
|
|
|
1109
1148
|
function drillGoDeeper(newLevel) {
|
|
1110
1149
|
saveDrillNavState();
|
|
1111
1150
|
drillLevel = newLevel;
|
|
1151
|
+
// Reset level-specific lifecycle filters when navigating
|
|
1152
|
+
if (newLevel === 'milestones') lifecycleFilterL2 = null;
|
|
1153
|
+
if (newLevel === 'actions') lifecycleFilterL3 = null;
|
|
1112
1154
|
pushDrillHash();
|
|
1113
1155
|
}
|
|
1114
1156
|
|
|
@@ -1119,6 +1161,128 @@ function drillGoBack(newLevel) {
|
|
|
1119
1161
|
pushDrillHash();
|
|
1120
1162
|
}
|
|
1121
1163
|
|
|
1164
|
+
/**
|
|
1165
|
+
* Navigate the drill browser to the result of a completed agent.
|
|
1166
|
+
* Maps agent type + result metadata to the correct drill state.
|
|
1167
|
+
* @param {object} agent - AgentRecord from the registry
|
|
1168
|
+
*/
|
|
1169
|
+
function navigateToResult(agent) {
|
|
1170
|
+
if (!graphData) return;
|
|
1171
|
+
const result = agent.result || {};
|
|
1172
|
+
const milestones = graphData.milestones || [];
|
|
1173
|
+
|
|
1174
|
+
switch (agent.type) {
|
|
1175
|
+
case 'execution': {
|
|
1176
|
+
// Navigate to the milestone's action list
|
|
1177
|
+
const mileId = result.milestoneId || agent.milestoneId;
|
|
1178
|
+
if (mileId) {
|
|
1179
|
+
const mile = milestones.find(m => m.id === mileId);
|
|
1180
|
+
if (mile && mile.realizes && mile.realizes.length) {
|
|
1181
|
+
drillDeclId = mile.realizes[0];
|
|
1182
|
+
}
|
|
1183
|
+
drillMileId = mileId;
|
|
1184
|
+
drillLevel = 'actions';
|
|
1185
|
+
}
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
case 'derivation': {
|
|
1189
|
+
// Navigate to the declaration's milestone list
|
|
1190
|
+
// The target is the declaration ID (e.g., "D-16") or "all"
|
|
1191
|
+
const declId = agent.target !== 'all' ? agent.target : null;
|
|
1192
|
+
if (declId) {
|
|
1193
|
+
drillDeclId = declId;
|
|
1194
|
+
drillLevel = 'milestones';
|
|
1195
|
+
} else {
|
|
1196
|
+
// "all" derivation — go to declarations list
|
|
1197
|
+
drillDeclId = null;
|
|
1198
|
+
drillMileId = null;
|
|
1199
|
+
drillLevel = 'declarations';
|
|
1200
|
+
}
|
|
1201
|
+
drillMileId = null;
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
case 'action-derivation': {
|
|
1205
|
+
// Navigate to the milestone's action list
|
|
1206
|
+
const mileId = result.milestoneId || agent.milestoneId || agent.target;
|
|
1207
|
+
if (mileId) {
|
|
1208
|
+
const mile = milestones.find(m => m.id === mileId);
|
|
1209
|
+
if (mile && mile.realizes && mile.realizes.length) {
|
|
1210
|
+
drillDeclId = mile.realizes[0];
|
|
1211
|
+
}
|
|
1212
|
+
drillMileId = mileId;
|
|
1213
|
+
drillLevel = 'actions';
|
|
1214
|
+
}
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
case 'revision': {
|
|
1218
|
+
// Navigate to the revised node — could be declaration or milestone
|
|
1219
|
+
const nodeId = result.nodeId || agent.target;
|
|
1220
|
+
if (nodeId && nodeId.startsWith('M-')) {
|
|
1221
|
+
// Milestone revision — navigate to its action list
|
|
1222
|
+
const mile = milestones.find(m => m.id === nodeId);
|
|
1223
|
+
if (mile && mile.realizes && mile.realizes.length) {
|
|
1224
|
+
drillDeclId = mile.realizes[0];
|
|
1225
|
+
}
|
|
1226
|
+
drillMileId = nodeId;
|
|
1227
|
+
drillLevel = 'actions';
|
|
1228
|
+
} else if (nodeId && nodeId.startsWith('D-')) {
|
|
1229
|
+
// Declaration revision — navigate to its milestone list
|
|
1230
|
+
drillDeclId = nodeId;
|
|
1231
|
+
drillMileId = null;
|
|
1232
|
+
drillLevel = 'milestones';
|
|
1233
|
+
}
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
case 'pipeline': {
|
|
1237
|
+
// Navigate to the milestone targeted by the pipeline
|
|
1238
|
+
const mileId = agent.milestoneId || agent.target;
|
|
1239
|
+
if (mileId && mileId.startsWith('M-')) {
|
|
1240
|
+
const mile = milestones.find(m => m.id === mileId);
|
|
1241
|
+
if (mile && mile.realizes && mile.realizes.length) {
|
|
1242
|
+
drillDeclId = mile.realizes[0];
|
|
1243
|
+
}
|
|
1244
|
+
drillMileId = mileId;
|
|
1245
|
+
drillLevel = 'actions';
|
|
1246
|
+
} else {
|
|
1247
|
+
drillDeclId = null;
|
|
1248
|
+
drillMileId = null;
|
|
1249
|
+
drillLevel = 'declarations';
|
|
1250
|
+
}
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
case 'refine':
|
|
1254
|
+
case 'discuss': {
|
|
1255
|
+
// Navigate to the refined/discussed node — same as revision
|
|
1256
|
+
const nodeId = result.nodeId || agent.target;
|
|
1257
|
+
if (nodeId && nodeId.startsWith('M-')) {
|
|
1258
|
+
const mile = milestones.find(m => m.id === nodeId);
|
|
1259
|
+
if (mile && mile.realizes && mile.realizes.length) {
|
|
1260
|
+
drillDeclId = mile.realizes[0];
|
|
1261
|
+
}
|
|
1262
|
+
drillMileId = nodeId;
|
|
1263
|
+
drillLevel = 'actions';
|
|
1264
|
+
} else if (nodeId && nodeId.startsWith('D-')) {
|
|
1265
|
+
drillDeclId = nodeId;
|
|
1266
|
+
drillMileId = null;
|
|
1267
|
+
drillLevel = 'milestones';
|
|
1268
|
+
}
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
default: {
|
|
1272
|
+
// Unknown agent type — fall back to declarations
|
|
1273
|
+
drillDeclId = null;
|
|
1274
|
+
drillMileId = null;
|
|
1275
|
+
drillLevel = 'declarations';
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Switch to columns view if not already there and render
|
|
1281
|
+
if (viewMode !== 'columns') switchView('columns');
|
|
1282
|
+
pushDrillHash();
|
|
1283
|
+
renderDrillView();
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1122
1286
|
/** Build the hash string for current drill state */
|
|
1123
1287
|
function drillHashString() {
|
|
1124
1288
|
let hash = '#/';
|
|
@@ -1177,6 +1341,13 @@ window.addEventListener('popstate', () => {
|
|
|
1177
1341
|
*/
|
|
1178
1342
|
function renderDrillView() {
|
|
1179
1343
|
if (!$drillBrowser || !graphData) return;
|
|
1344
|
+
|
|
1345
|
+
// If onboarding is active, render it instead
|
|
1346
|
+
if (onboardPhase !== 'idle') {
|
|
1347
|
+
renderOnboardUI();
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1180
1351
|
syncDrillHash(); // Keep URL in sync without creating history entries
|
|
1181
1352
|
|
|
1182
1353
|
const { declarations, milestones, actions } = graphData;
|
|
@@ -1229,6 +1400,11 @@ function renderDrillView() {
|
|
|
1229
1400
|
updateStatusBreadcrumb(enrichedDeclarations, enrichedMilestones);
|
|
1230
1401
|
// Update next/execute button
|
|
1231
1402
|
updateNextButton(enrichedDeclarations, enrichedMilestones, actions || []);
|
|
1403
|
+
|
|
1404
|
+
// Re-attach discuss container if a discuss session is active (survives re-renders)
|
|
1405
|
+
if (discussActiveNodeId && discussActiveContainer) {
|
|
1406
|
+
reattachDiscussContainer();
|
|
1407
|
+
}
|
|
1232
1408
|
}
|
|
1233
1409
|
|
|
1234
1410
|
/**
|
|
@@ -1276,47 +1452,100 @@ function updateStatusBreadcrumb(enrichedDeclarations, enrichedMilestones) {
|
|
|
1276
1452
|
|
|
1277
1453
|
/**
|
|
1278
1454
|
* Find the next unapproved node and update the main button.
|
|
1455
|
+
* Uses lifecycle nextAction when available for smarter guidance.
|
|
1279
1456
|
*/
|
|
1280
1457
|
function updateNextButton(enrichedDeclarations, enrichedMilestones, actions) {
|
|
1281
1458
|
if (!$executeMainBtn) return;
|
|
1282
1459
|
|
|
1283
|
-
//
|
|
1284
|
-
const
|
|
1285
|
-
const unapprovedMile = enrichedMilestones.find(m => m.reviewState !== 'approved');
|
|
1286
|
-
const unapprovedAction = (actions || []).find(a => a.reviewState !== 'approved' && !COMPLETED.has((a.status || '').toUpperCase()));
|
|
1287
|
-
|
|
1288
|
-
const hasUnapproved = unapprovedDecl || unapprovedMile || unapprovedAction;
|
|
1460
|
+
// Use lifecycle nextAction if available
|
|
1461
|
+
const next = lifecycleData ? lifecycleData.nextAction : null;
|
|
1289
1462
|
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
$executeMainBtn.id = 'execute-main-btn';
|
|
1294
|
-
$executeMainBtn.disabled = false;
|
|
1295
|
-
$executeMainBtn.title = 'Go to next item needing approval';
|
|
1296
|
-
|
|
1297
|
-
// Store target for click handler
|
|
1298
|
-
$executeMainBtn._nextTarget = { decl: unapprovedDecl, mile: unapprovedMile, action: unapprovedAction };
|
|
1299
|
-
$executeMainBtn._planMode = false;
|
|
1300
|
-
} else {
|
|
1301
|
-
// Check if any declarations still need milestones (PENDING = no milestones planned)
|
|
1302
|
-
const needsPlanning = enrichedDeclarations.some(d => d.displayStatus === 'PENDING');
|
|
1303
|
-
|
|
1304
|
-
if (needsPlanning) {
|
|
1305
|
-
$executeMainBtn.innerHTML = '<kbd>P</kbd> Plan';
|
|
1463
|
+
if (next) {
|
|
1464
|
+
if (next.action === 'derive-milestones' || next.action === 'derive-actions') {
|
|
1465
|
+
$executeMainBtn.innerHTML = '<kbd>⌃⇧P</kbd> Plan';
|
|
1306
1466
|
$executeMainBtn.className = 'btn-plan';
|
|
1307
1467
|
$executeMainBtn.id = 'execute-main-btn';
|
|
1308
1468
|
$executeMainBtn.disabled = false;
|
|
1309
|
-
$executeMainBtn.title =
|
|
1469
|
+
$executeMainBtn.title = next.label;
|
|
1310
1470
|
$executeMainBtn._nextTarget = null;
|
|
1311
1471
|
$executeMainBtn._planMode = true;
|
|
1312
|
-
|
|
1472
|
+
$executeMainBtn._lifecycleAction = next;
|
|
1473
|
+
} else if (next.action === 'approve') {
|
|
1474
|
+
$executeMainBtn.innerHTML = '<kbd>N</kbd> Next';
|
|
1475
|
+
$executeMainBtn.className = 'btn-next';
|
|
1476
|
+
$executeMainBtn.id = 'execute-main-btn';
|
|
1477
|
+
$executeMainBtn.disabled = false;
|
|
1478
|
+
$executeMainBtn.title = next.label;
|
|
1479
|
+
$executeMainBtn._nextTarget = null;
|
|
1480
|
+
$executeMainBtn._planMode = false;
|
|
1481
|
+
$executeMainBtn._lifecycleAction = next;
|
|
1482
|
+
} else if (next.action === 'execute') {
|
|
1313
1483
|
$executeMainBtn.innerHTML = '<kbd>E</kbd> Execute';
|
|
1314
1484
|
$executeMainBtn.className = '';
|
|
1315
1485
|
$executeMainBtn.id = 'execute-main-btn';
|
|
1316
1486
|
$executeMainBtn.disabled = !canEnterExecution();
|
|
1317
|
-
$executeMainBtn.title =
|
|
1487
|
+
$executeMainBtn.title = next.label;
|
|
1318
1488
|
$executeMainBtn._nextTarget = null;
|
|
1319
1489
|
$executeMainBtn._planMode = false;
|
|
1490
|
+
$executeMainBtn._lifecycleAction = next;
|
|
1491
|
+
} else if (next.action === 'complete') {
|
|
1492
|
+
$executeMainBtn.innerHTML = '\u2713 Done';
|
|
1493
|
+
$executeMainBtn.className = 'btn-done';
|
|
1494
|
+
$executeMainBtn.id = 'execute-main-btn';
|
|
1495
|
+
$executeMainBtn.disabled = true;
|
|
1496
|
+
$executeMainBtn.title = 'All items complete';
|
|
1497
|
+
$executeMainBtn._nextTarget = null;
|
|
1498
|
+
$executeMainBtn._planMode = false;
|
|
1499
|
+
$executeMainBtn._lifecycleAction = null;
|
|
1500
|
+
} else {
|
|
1501
|
+
// view-execution or other — show status
|
|
1502
|
+
$executeMainBtn.innerHTML = next.label;
|
|
1503
|
+
$executeMainBtn.className = '';
|
|
1504
|
+
$executeMainBtn.id = 'execute-main-btn';
|
|
1505
|
+
$executeMainBtn.disabled = true;
|
|
1506
|
+
$executeMainBtn.title = next.label;
|
|
1507
|
+
$executeMainBtn._nextTarget = null;
|
|
1508
|
+
$executeMainBtn._planMode = false;
|
|
1509
|
+
$executeMainBtn._lifecycleAction = null;
|
|
1510
|
+
}
|
|
1511
|
+
} else {
|
|
1512
|
+
// Fallback to original logic when lifecycle data not available
|
|
1513
|
+
const unapprovedDecl = enrichedDeclarations.find(d => d.reviewState !== 'approved');
|
|
1514
|
+
const unapprovedMile = enrichedMilestones.find(m => m.reviewState !== 'approved');
|
|
1515
|
+
const unapprovedAction = (actions || []).find(a => a.reviewState !== 'approved' && !COMPLETED.has((a.status || '').toUpperCase()));
|
|
1516
|
+
|
|
1517
|
+
const hasUnapproved = unapprovedDecl || unapprovedMile || unapprovedAction;
|
|
1518
|
+
|
|
1519
|
+
if (hasUnapproved) {
|
|
1520
|
+
$executeMainBtn.innerHTML = '<kbd>N</kbd> Next';
|
|
1521
|
+
$executeMainBtn.className = 'btn-next';
|
|
1522
|
+
$executeMainBtn.id = 'execute-main-btn';
|
|
1523
|
+
$executeMainBtn.disabled = false;
|
|
1524
|
+
$executeMainBtn.title = 'Go to next item needing approval';
|
|
1525
|
+
$executeMainBtn._nextTarget = { decl: unapprovedDecl, mile: unapprovedMile, action: unapprovedAction };
|
|
1526
|
+
$executeMainBtn._planMode = false;
|
|
1527
|
+
$executeMainBtn._lifecycleAction = null;
|
|
1528
|
+
} else {
|
|
1529
|
+
const needsPlanning = enrichedDeclarations.some(d => d.displayStatus === 'PENDING');
|
|
1530
|
+
if (needsPlanning) {
|
|
1531
|
+
$executeMainBtn.innerHTML = '<kbd>⌃⇧P</kbd> Plan';
|
|
1532
|
+
$executeMainBtn.className = 'btn-plan';
|
|
1533
|
+
$executeMainBtn.id = 'execute-main-btn';
|
|
1534
|
+
$executeMainBtn.disabled = false;
|
|
1535
|
+
$executeMainBtn.title = 'Plan milestones for unplanned declarations';
|
|
1536
|
+
$executeMainBtn._nextTarget = null;
|
|
1537
|
+
$executeMainBtn._planMode = true;
|
|
1538
|
+
$executeMainBtn._lifecycleAction = null;
|
|
1539
|
+
} else {
|
|
1540
|
+
$executeMainBtn.innerHTML = '<kbd>E</kbd> Execute';
|
|
1541
|
+
$executeMainBtn.className = '';
|
|
1542
|
+
$executeMainBtn.id = 'execute-main-btn';
|
|
1543
|
+
$executeMainBtn.disabled = !canEnterExecution();
|
|
1544
|
+
$executeMainBtn.title = canEnterExecution() ? 'Enter execution mode' : 'No actions to execute';
|
|
1545
|
+
$executeMainBtn._nextTarget = null;
|
|
1546
|
+
$executeMainBtn._planMode = false;
|
|
1547
|
+
$executeMainBtn._lifecycleAction = null;
|
|
1548
|
+
}
|
|
1320
1549
|
}
|
|
1321
1550
|
}
|
|
1322
1551
|
|
|
@@ -1358,19 +1587,52 @@ function renderProjectDetail() {
|
|
|
1358
1587
|
const mCount = stats.milestones || 0;
|
|
1359
1588
|
const aCount = stats.actions || 0;
|
|
1360
1589
|
|
|
1590
|
+
// Lifecycle progress if available
|
|
1591
|
+
let progressHtml = '';
|
|
1592
|
+
if (lifecycleData) {
|
|
1593
|
+
const s = lifecycleData.stages;
|
|
1594
|
+
const total = Object.values(s).reduce((sum, arr) => sum + arr.length, 0);
|
|
1595
|
+
if (total > 0) {
|
|
1596
|
+
const segments = [
|
|
1597
|
+
{ count: (s['needs-planning'] || []).length, color: '#fbbf24' },
|
|
1598
|
+
{ count: (s['needs-approval'] || []).length, color: '#f97316' },
|
|
1599
|
+
{ count: (s['ready-to-execute'] || []).length, color: '#86efac' },
|
|
1600
|
+
{ count: (s['in-execution'] || []).length, color: '#60a5fa' },
|
|
1601
|
+
{ count: (s['done'] || []).length, color: '#6b7280' },
|
|
1602
|
+
];
|
|
1603
|
+
progressHtml = `<div class="detail-section-label">Progress</div>
|
|
1604
|
+
<div class="lifecycle-progress">${segments.map(seg =>
|
|
1605
|
+
seg.count > 0 ? `<div class="lifecycle-progress-segment" style="width:${(seg.count / total * 100).toFixed(1)}%;background:${seg.color}"></div>` : ''
|
|
1606
|
+
).join('')}</div>
|
|
1607
|
+
<div class="detail-meta">${lifecycleData.progress.done}/${lifecycleData.progress.total} done (${lifecycleData.progress.percentage}%)</div>`;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1361
1611
|
$drillDetail.innerHTML = `
|
|
1362
1612
|
<div class="detail-id" style="color:var(--accent)">PROJECT</div>
|
|
1363
1613
|
<div class="detail-title">${escHtml(title)}</div>
|
|
1364
1614
|
${desc ? `<div class="detail-desc">${escHtml(desc)}</div>` : '<div class="detail-desc" style="color:var(--text-dim);font-style:italic">No project description yet. Run /declare:new-project to set one up.</div>'}
|
|
1365
1615
|
${core ? `<div class="detail-section-label">Core Value</div><div class="detail-desc">${escHtml(core)}</div>` : ''}
|
|
1366
1616
|
${currentState ? `<div class="detail-section-label">Current State</div><div class="detail-meta">${escHtml(currentState)}</div>` : ''}
|
|
1617
|
+
${progressHtml}
|
|
1367
1618
|
<div class="detail-section-label">Graph</div>
|
|
1368
1619
|
<div class="detail-meta">${dCount} declarations · ${mCount} milestones · ${aCount} actions</div>
|
|
1369
1620
|
`;
|
|
1370
1621
|
}
|
|
1371
1622
|
|
|
1623
|
+
// ─── Lifecycle stage display config ──────────────────────────────────────────
|
|
1624
|
+
|
|
1625
|
+
const LIFECYCLE_STAGES = [
|
|
1626
|
+
{ key: 'needs-planning', label: 'Needs Planning', color: '#fbbf24', icon: '\u270E' },
|
|
1627
|
+
{ key: 'needs-approval', label: 'Needs Approval', color: '#f97316', icon: '\u2691' },
|
|
1628
|
+
{ key: 'ready-to-execute', label: 'Ready to Execute', color: '#86efac', icon: '\u25B6' },
|
|
1629
|
+
{ key: 'in-execution', label: 'In Execution', color: '#60a5fa', icon: '\u2699' },
|
|
1630
|
+
{ key: 'done', label: 'Done', color: '#6b7280', icon: '\u2713' },
|
|
1631
|
+
];
|
|
1632
|
+
|
|
1633
|
+
|
|
1372
1634
|
/**
|
|
1373
|
-
* Level 1 —
|
|
1635
|
+
* Level 1 — Lifecycle-grouped view: all items grouped by stage.
|
|
1374
1636
|
*/
|
|
1375
1637
|
function renderDrillDeclarations(enrichedDeclarations, enrichedMilestones, actions) {
|
|
1376
1638
|
$drillContext.innerHTML = '';
|
|
@@ -1380,77 +1642,499 @@ function renderDrillDeclarations(enrichedDeclarations, enrichedMilestones, actio
|
|
|
1380
1642
|
$drillDetail.classList.add('visible');
|
|
1381
1643
|
renderProjectDetail();
|
|
1382
1644
|
|
|
1645
|
+
// Handle empty project — delegate to onboarding (M-56)
|
|
1383
1646
|
if (enrichedDeclarations.length === 0) {
|
|
1384
|
-
|
|
1385
|
-
$drillPrompt.innerHTML = '';
|
|
1647
|
+
renderEmptyOnboarding();
|
|
1386
1648
|
return;
|
|
1387
1649
|
}
|
|
1388
1650
|
|
|
1651
|
+
// Classify ONLY declarations into lifecycle stages
|
|
1652
|
+
const stages = classifyDeclarationsToStages(enrichedDeclarations, enrichedMilestones, actions);
|
|
1653
|
+
const nextAction = lifecycleData ? lifecycleData.nextAction : null;
|
|
1654
|
+
|
|
1655
|
+
// Render filter chips
|
|
1656
|
+
renderLifecycleFilterChipsGeneric($drillContext, stages, lifecycleFilter, (f) => {
|
|
1657
|
+
lifecycleFilter = f;
|
|
1658
|
+
renderDrillView();
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
// Build declaration cards grouped by lifecycle stage
|
|
1389
1662
|
let firstUnapproved = null;
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1663
|
+
const wrapper = document.createElement('div');
|
|
1664
|
+
wrapper.className = 'lifecycle-stages';
|
|
1665
|
+
|
|
1666
|
+
LIFECYCLE_STAGES.forEach(stageDef => {
|
|
1667
|
+
const items = stages[stageDef.key] || [];
|
|
1668
|
+
if (lifecycleFilter && lifecycleFilter !== stageDef.key) return;
|
|
1669
|
+
if (items.length === 0 && !lifecycleFilter) return;
|
|
1670
|
+
|
|
1671
|
+
const isDone = stageDef.key === 'done';
|
|
1672
|
+
const isCollapsed = isDone && lifecycleDoneCollapsed && !lifecycleFilter;
|
|
1673
|
+
|
|
1674
|
+
const section = document.createElement('div');
|
|
1675
|
+
section.className = `lifecycle-section${isCollapsed ? ' collapsed' : ''}`;
|
|
1676
|
+
section.dataset.stage = stageDef.key;
|
|
1677
|
+
|
|
1678
|
+
// Stage header with action buttons
|
|
1679
|
+
const header = document.createElement('div');
|
|
1680
|
+
header.className = 'lifecycle-header';
|
|
1681
|
+
|
|
1682
|
+
let stageBtnHtml = '';
|
|
1683
|
+
if (stageDef.key === 'needs-planning' && items.length > 0) {
|
|
1684
|
+
stageBtnHtml = '<button class="lifecycle-stage-btn" data-stage-action="plan-next">Plan Next</button>';
|
|
1685
|
+
stageBtnHtml += `<button class="lifecycle-stage-btn" data-stage-action="plan-all">Plan All (${items.length})</button>`;
|
|
1686
|
+
} else if (stageDef.key === 'ready-to-execute' && items.length > 0) {
|
|
1687
|
+
stageBtnHtml = '<button class="lifecycle-stage-btn" data-stage-action="execute">Execute</button>';
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
header.innerHTML = `
|
|
1691
|
+
<span class="lifecycle-icon" style="color:${stageDef.color}">${stageDef.icon}</span>
|
|
1692
|
+
<span class="lifecycle-label">${escHtml(stageDef.label)}</span>
|
|
1693
|
+
<span class="lifecycle-count" style="background:${stageDef.color}20;color:${stageDef.color}">${items.length}</span>
|
|
1694
|
+
${isDone ? `<span class="lifecycle-toggle">${isCollapsed ? '\u25B6' : '\u25BC'}</span>` : ''}
|
|
1695
|
+
<span class="lifecycle-header-spacer"></span>
|
|
1696
|
+
${stageBtnHtml}
|
|
1697
|
+
`;
|
|
1698
|
+
if (isDone) {
|
|
1699
|
+
header.style.cursor = 'pointer';
|
|
1700
|
+
header.addEventListener('click', (e) => {
|
|
1701
|
+
if (e.target.closest('.lifecycle-stage-btn')) return;
|
|
1702
|
+
lifecycleDoneCollapsed = !lifecycleDoneCollapsed;
|
|
1703
|
+
renderDrillView();
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1393
1706
|
|
|
1394
|
-
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1707
|
+
// Wire stage action buttons
|
|
1708
|
+
const stageBtn = header.querySelector('.lifecycle-stage-btn');
|
|
1709
|
+
if (stageBtn) {
|
|
1710
|
+
stageBtn.addEventListener('click', (e) => {
|
|
1711
|
+
e.stopPropagation();
|
|
1712
|
+
const action = stageBtn.dataset.stageAction;
|
|
1713
|
+
if (action === 'plan-next') {
|
|
1714
|
+
// Navigate into the first declaration needing planning
|
|
1715
|
+
const first = items[0];
|
|
1716
|
+
if (first) {
|
|
1717
|
+
drillDeclId = first.id;
|
|
1718
|
+
drillGoDeeper('milestones');
|
|
1719
|
+
renderDrillView();
|
|
1720
|
+
}
|
|
1721
|
+
} else if (action === 'plan-all') {
|
|
1722
|
+
// Kick off concurrent derivation for all declarations needing planning
|
|
1723
|
+
triggerDeriveAll();
|
|
1724
|
+
} else if (action === 'execute') {
|
|
1725
|
+
switchView('execution');
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1401
1729
|
|
|
1402
|
-
|
|
1403
|
-
const statusClass = d.displayStatus === 'DONE' ? 's-done' : d.displayStatus === 'EXECUTING' ? 's-executing' : 's-planned';
|
|
1730
|
+
section.appendChild(header);
|
|
1404
1731
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1732
|
+
// Cards — declaration cards with statement text
|
|
1733
|
+
if (!isCollapsed) {
|
|
1734
|
+
const cardsContainer = document.createElement('div');
|
|
1735
|
+
cardsContainer.className = 'drill-cards lifecycle-cards';
|
|
1736
|
+
cardsContainer.dataset.nodeType = 'declaration';
|
|
1737
|
+
|
|
1738
|
+
items.forEach(d => {
|
|
1739
|
+
const needsReview = d.reviewState !== 'approved';
|
|
1740
|
+
const isCurrent = needsReview && !firstUnapproved;
|
|
1741
|
+
if (needsReview && !firstUnapproved) firstUnapproved = d;
|
|
1742
|
+
|
|
1743
|
+
const stmt = d.statement || d.title || '';
|
|
1744
|
+
const myMiles = enrichedMilestones.filter(m => (m.realizes || []).includes(d.id));
|
|
1745
|
+
const mileCount = myMiles.length;
|
|
1746
|
+
const unapprovedMiles = myMiles.filter(m => m.reviewState !== 'approved').length;
|
|
1747
|
+
|
|
1748
|
+
const isDeriving = activeDeriveMap.has(d.id);
|
|
1749
|
+
const runningAgent = getRunningAgentForNode(d.id);
|
|
1750
|
+
const isPending = pendingDerivations.has(d.id);
|
|
1751
|
+
const hasActiveWork = isDeriving || runningAgent || isPending;
|
|
1752
|
+
|
|
1753
|
+
let statusLabel = d.displayStatus || d.status || 'PENDING';
|
|
1754
|
+
const statusClass = statusLabel === 'DONE' ? 's-done' : statusLabel === 'EXECUTING' ? 's-executing' : 's-planned';
|
|
1755
|
+
|
|
1756
|
+
let badgesHtml = `<span class="drill-status-pill ${statusClass}">${escHtml(statusLabel)}</span>`;
|
|
1757
|
+
badgesHtml += reviewBadgeHtml(d.id, d.reviewState);
|
|
1758
|
+
if (mileCount > 0) {
|
|
1759
|
+
badgesHtml += `<span class="drill-card-stat">${mileCount} milestones`;
|
|
1760
|
+
if (unapprovedMiles > 0) badgesHtml += ` <strong>(${unapprovedMiles} need review)</strong>`;
|
|
1761
|
+
badgesHtml += '</span>';
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const activeLabel = isPending ? 'Starting\u2026' : isDeriving ? 'Planning\u2026' : runningAgent ? (AGENT_TYPE_LABELS[runningAgent.type] || runningAgent.type) + '\u2026' : '';
|
|
1765
|
+
|
|
1766
|
+
const el = document.createElement('div');
|
|
1767
|
+
el.className = `drill-card${needsReview ? ' needs-review' : ''}${isCurrent ? ' current-review' : ''}${hasActiveWork ? ' drill-card-deriving' : ''}`;
|
|
1768
|
+
el.innerHTML = `
|
|
1769
|
+
<div class="drill-card-top">
|
|
1770
|
+
<span class="drill-card-id">${escHtml(d.id)}</span>
|
|
1771
|
+
<div class="drill-card-body">
|
|
1772
|
+
<div class="drill-card-title">${escHtml(d.title || d.statement || d.id)}</div>
|
|
1773
|
+
<div class="drill-card-desc">${stmt !== (d.title || '') ? escHtml(stmt) : ''}</div>
|
|
1774
|
+
<div class="drill-card-badges">${badgesHtml}</div>
|
|
1775
|
+
</div>
|
|
1776
|
+
</div>
|
|
1777
|
+
${renderEntityActions(d.id, d.reviewState, undefined, hasActiveWork ? activeLabel : null)}
|
|
1778
|
+
`;
|
|
1779
|
+
|
|
1780
|
+
el.addEventListener('click', (e) => {
|
|
1781
|
+
if (e.target.closest('.drill-review-btn') || e.target.closest('.drill-action-btn') || e.target.closest('textarea') || e.target.closest('input') || e.target.closest('.refine-container') || e.target.closest('.discuss-container')) return;
|
|
1782
|
+
drillDeclId = d.id;
|
|
1783
|
+
drillGoDeeper('milestones');
|
|
1784
|
+
renderDrillView();
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
cardsContainer.appendChild(el);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
section.appendChild(cardsContainer);
|
|
1791
|
+
wireInlineReviewButtons(cardsContainer);
|
|
1792
|
+
wireEntityMenus(cardsContainer);
|
|
1411
1793
|
}
|
|
1412
1794
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
el.innerHTML = `
|
|
1416
|
-
<div class="drill-card-top">
|
|
1417
|
-
<span class="drill-card-id">${escHtml(d.id)}</span>
|
|
1418
|
-
<div class="drill-card-body">
|
|
1419
|
-
<div class="drill-card-title">${escHtml(title)}</div>
|
|
1420
|
-
<div class="drill-card-desc">${desc !== title ? escHtml(desc) : ''}</div>
|
|
1421
|
-
<div class="drill-card-badges">${badgesHtml}</div>
|
|
1422
|
-
</div>
|
|
1423
|
-
</div>
|
|
1424
|
-
${renderEntityActions(d.id, d.reviewState)}
|
|
1425
|
-
`;
|
|
1795
|
+
wrapper.appendChild(section);
|
|
1796
|
+
});
|
|
1426
1797
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1798
|
+
$drillList.appendChild(wrapper);
|
|
1799
|
+
|
|
1800
|
+
$drillPrompt.innerHTML = '';
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Navigate into a lifecycle item by type.
|
|
1806
|
+
*/
|
|
1807
|
+
function navigateToItem(item, enrichedDeclarations, enrichedMilestones, actions) {
|
|
1808
|
+
if (item.type === 'declaration') {
|
|
1809
|
+
drillDeclId = item.id;
|
|
1810
|
+
drillGoDeeper('milestones');
|
|
1811
|
+
renderDrillView();
|
|
1812
|
+
} else if (item.type === 'milestone') {
|
|
1813
|
+
// Find parent declaration
|
|
1814
|
+
const decl = enrichedDeclarations.find(d => (d.milestones || []).includes(item.id));
|
|
1815
|
+
if (decl) drillDeclId = decl.id;
|
|
1816
|
+
drillMileId = item.id;
|
|
1817
|
+
drillGoDeeper('actions');
|
|
1818
|
+
renderDrillView();
|
|
1819
|
+
} else if (item.type === 'action') {
|
|
1820
|
+
// Find parent milestone and declaration
|
|
1821
|
+
const action = (actions || []).find(a => a.id === item.id);
|
|
1822
|
+
if (action) {
|
|
1823
|
+
const mId = (action.causes || [])[0];
|
|
1824
|
+
if (mId) {
|
|
1825
|
+
const mile = enrichedMilestones.find(m => m.id === mId);
|
|
1826
|
+
if (mile && mile.realizes && mile.realizes.length) drillDeclId = mile.realizes[0];
|
|
1827
|
+
drillMileId = mId;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
drillGoDeeper('actions');
|
|
1831
|
+
renderDrillView();
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Render the prompt bar for lifecycle view using nextAction intelligence.
|
|
1837
|
+
*/
|
|
1838
|
+
function renderLifecyclePrompt(nextAction, enrichedDeclarations, enrichedMilestones, actions) {
|
|
1839
|
+
if (!$drillPrompt) return;
|
|
1840
|
+
|
|
1841
|
+
if (!nextAction) {
|
|
1842
|
+
$drillPrompt.innerHTML = '';
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
if (nextAction.action === 'complete') {
|
|
1847
|
+
$drillPrompt.innerHTML = `<span class="drill-prompt-complete">${escHtml(nextAction.label)}</span>`;
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// Count unapproved items across all types
|
|
1852
|
+
const allNodes = [...enrichedDeclarations, ...enrichedMilestones, ...(actions || [])];
|
|
1853
|
+
const unapprovedCount = allNodes.filter(n => n.reviewState !== 'approved' && !COMPLETED.has((n.status || '').toUpperCase())).length;
|
|
1854
|
+
|
|
1855
|
+
const labelHtml = `<span class="drill-prompt-text">\u2192 ${escHtml(nextAction.label)}</span>`;
|
|
1856
|
+
|
|
1857
|
+
let actionsHtml = '';
|
|
1858
|
+
if (nextAction.targetId) {
|
|
1859
|
+
actionsHtml += `<button class="drill-next-btn" id="drill-lifecycle-go"><kbd>N</kbd> Go</button>`;
|
|
1860
|
+
}
|
|
1861
|
+
if (unapprovedCount > 0) {
|
|
1862
|
+
actionsHtml += `<button class="drill-approve-all-btn" id="drill-approve-all"><kbd>⌃⇧A</kbd> Approve All (${unapprovedCount})</button>`;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
$drillPrompt.innerHTML = labelHtml + actionsHtml;
|
|
1866
|
+
|
|
1867
|
+
// Wire go button
|
|
1868
|
+
const goBtn = document.getElementById('drill-lifecycle-go');
|
|
1869
|
+
if (goBtn && nextAction.targetId) {
|
|
1870
|
+
goBtn.addEventListener('click', () => {
|
|
1871
|
+
if (nextAction.action === 'derive-milestones') {
|
|
1872
|
+
drillDeclId = nextAction.targetId;
|
|
1873
|
+
drillGoDeeper('milestones');
|
|
1874
|
+
renderDrillView();
|
|
1875
|
+
} else if (nextAction.action === 'derive-actions') {
|
|
1876
|
+
// Find parent declaration
|
|
1877
|
+
const mile = enrichedMilestones.find(m => m.id === nextAction.targetId);
|
|
1878
|
+
if (mile && mile.realizes && mile.realizes.length) drillDeclId = mile.realizes[0];
|
|
1879
|
+
drillMileId = nextAction.targetId;
|
|
1880
|
+
drillGoDeeper('actions');
|
|
1881
|
+
renderDrillView();
|
|
1882
|
+
} else if (nextAction.action === 'approve') {
|
|
1883
|
+
// Navigate to the node needing approval
|
|
1884
|
+
navigateToItem({
|
|
1885
|
+
id: nextAction.targetId,
|
|
1886
|
+
type: nextAction.targetType || 'declaration',
|
|
1887
|
+
}, enrichedDeclarations, enrichedMilestones, actions);
|
|
1888
|
+
} else if (nextAction.action === 'execute') {
|
|
1889
|
+
switchView('execution');
|
|
1890
|
+
}
|
|
1432
1891
|
});
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// Wire approve-all
|
|
1895
|
+
const approveAllBtn = document.getElementById('drill-approve-all');
|
|
1896
|
+
if (approveAllBtn) {
|
|
1897
|
+
approveAllBtn.addEventListener('click', () => approveAllVisible());
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Classify declarations into lifecycle stages (declarations only, no mixing).
|
|
1903
|
+
*/
|
|
1904
|
+
function classifyDeclarationsToStages(declarations, milestones, actions) {
|
|
1905
|
+
const stages = { 'needs-planning': [], 'needs-approval': [], 'ready-to-execute': [], 'in-execution': [], 'done': [] };
|
|
1906
|
+
for (const d of declarations) {
|
|
1907
|
+
const status = (d.status || '').toUpperCase();
|
|
1908
|
+
if (COMPLETED.has(status) || d.displayStatus === 'DONE') {
|
|
1909
|
+
stages['done'].push(d);
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// A declaration's stage is determined by the worst stage of its children.
|
|
1914
|
+
// No milestones at all → needs planning.
|
|
1915
|
+
const myMiles = milestones.filter(m => (m.realizes || []).includes(d.id));
|
|
1916
|
+
if (myMiles.length === 0) {
|
|
1917
|
+
stages['needs-planning'].push(d);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Check each milestone's stage and bubble up the worst one.
|
|
1922
|
+
// Priority (worst to best): needs-planning > needs-approval > ready-to-execute > in-execution > done
|
|
1923
|
+
let worstStage = 'done';
|
|
1924
|
+
const stageRank = { 'needs-planning': 0, 'needs-approval': 1, 'ready-to-execute': 2, 'in-execution': 3, 'done': 4 };
|
|
1925
|
+
|
|
1926
|
+
for (const m of myMiles) {
|
|
1927
|
+
const mStatus = (m.status || m.displayStatus || '').toUpperCase();
|
|
1928
|
+
let mStage;
|
|
1929
|
+
if (COMPLETED.has(mStatus) || m.displayStatus === 'DONE') {
|
|
1930
|
+
mStage = 'done';
|
|
1931
|
+
} else if (!(actions || []).some(a => (a.causes || []).includes(m.id))) {
|
|
1932
|
+
mStage = 'needs-planning';
|
|
1933
|
+
} else if (m.reviewState !== 'approved') {
|
|
1934
|
+
mStage = 'needs-approval';
|
|
1935
|
+
} else if (m.displayStatus === 'EXECUTING' || EXECUTING_STATUSES_SET.has(mStatus)) {
|
|
1936
|
+
mStage = 'in-execution';
|
|
1937
|
+
} else {
|
|
1938
|
+
mStage = 'ready-to-execute';
|
|
1939
|
+
}
|
|
1940
|
+
if (stageRank[mStage] < stageRank[worstStage]) {
|
|
1941
|
+
worstStage = mStage;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Declaration's own review state can also pull it back
|
|
1946
|
+
if (d.reviewState !== 'approved' && stageRank['needs-approval'] < stageRank[worstStage]) {
|
|
1947
|
+
worstStage = 'needs-approval';
|
|
1948
|
+
}
|
|
1433
1949
|
|
|
1434
|
-
|
|
1950
|
+
stages[worstStage].push(d);
|
|
1951
|
+
}
|
|
1952
|
+
return stages;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Classify a list of milestones into lifecycle stages.
|
|
1957
|
+
*/
|
|
1958
|
+
function classifyMilestonesToStages(milestones, actions) {
|
|
1959
|
+
const stages = { 'needs-planning': [], 'needs-approval': [], 'ready-to-execute': [], 'in-execution': [], 'done': [] };
|
|
1960
|
+
for (const m of milestones) {
|
|
1961
|
+
const status = (m.status || m.displayStatus || '').toUpperCase();
|
|
1962
|
+
if (COMPLETED.has(status) || m.displayStatus === 'DONE') {
|
|
1963
|
+
stages['done'].push(m);
|
|
1964
|
+
} else {
|
|
1965
|
+
const myActions = (actions || []).filter(a => (a.causes || []).includes(m.id));
|
|
1966
|
+
const hasActions = myActions.length > 0;
|
|
1967
|
+
const hasNoActionPlan = !m.hasPlan && !hasActions;
|
|
1968
|
+
const allActionsApproved = hasActions && myActions.every(a => a.reviewState === 'approved' || COMPLETED.has((a.status || '').toUpperCase()));
|
|
1969
|
+
const hasUnapprovedActions = hasActions && !allActionsApproved;
|
|
1970
|
+
if (hasNoActionPlan) {
|
|
1971
|
+
stages['needs-planning'].push(m);
|
|
1972
|
+
} else if (m.reviewState !== 'approved' || hasUnapprovedActions) {
|
|
1973
|
+
stages['needs-approval'].push(m);
|
|
1974
|
+
} else if (m.displayStatus === 'EXECUTING' || EXECUTING_STATUSES_SET.has(status)) {
|
|
1975
|
+
stages['in-execution'].push(m);
|
|
1976
|
+
} else {
|
|
1977
|
+
stages['ready-to-execute'].push(m);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return stages;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Classify a list of actions into lifecycle stages.
|
|
1986
|
+
*/
|
|
1987
|
+
function classifyActionsToStages(actionsArr) {
|
|
1988
|
+
const stages = { 'needs-planning': [], 'needs-approval': [], 'ready-to-execute': [], 'in-execution': [], 'done': [] };
|
|
1989
|
+
for (const a of actionsArr) {
|
|
1990
|
+
const status = (a.status || '').toUpperCase();
|
|
1991
|
+
if (COMPLETED.has(status)) {
|
|
1992
|
+
stages['done'].push(a);
|
|
1993
|
+
} else if (runningActions.has(a.id) || EXECUTING_STATUSES_SET.has(status)) {
|
|
1994
|
+
stages['in-execution'].push(a);
|
|
1995
|
+
} else if (a.reviewState !== 'approved') {
|
|
1996
|
+
stages['needs-approval'].push(a);
|
|
1997
|
+
} else {
|
|
1998
|
+
stages['ready-to-execute'].push(a);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return stages;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Render lifecycle filter chips into a container for any level.
|
|
2006
|
+
* @param {HTMLElement} container - Element to render chips into
|
|
2007
|
+
* @param {Object} stages - Stage classification
|
|
2008
|
+
* @param {string|null} activeFilter - Current filter
|
|
2009
|
+
* @param {function} onFilter - Callback when filter changes
|
|
2010
|
+
*/
|
|
2011
|
+
function renderLifecycleFilterChipsGeneric(container, stages, activeFilter, onFilter) {
|
|
2012
|
+
const chips = document.createElement('div');
|
|
2013
|
+
chips.className = 'lifecycle-filter-chips';
|
|
2014
|
+
|
|
2015
|
+
const allChip = document.createElement('button');
|
|
2016
|
+
allChip.className = `lifecycle-chip${!activeFilter ? ' active' : ''}`;
|
|
2017
|
+
allChip.textContent = 'All';
|
|
2018
|
+
allChip.addEventListener('click', () => onFilter(null));
|
|
2019
|
+
chips.appendChild(allChip);
|
|
2020
|
+
|
|
2021
|
+
LIFECYCLE_STAGES.forEach(stageDef => {
|
|
2022
|
+
const items = stages[stageDef.key] || [];
|
|
2023
|
+
if (items.length === 0) return;
|
|
2024
|
+
const chip = document.createElement('button');
|
|
2025
|
+
chip.className = `lifecycle-chip${activeFilter === stageDef.key ? ' active' : ''}`;
|
|
2026
|
+
chip.innerHTML = `${escHtml(stageDef.label)} <span class="chip-count">${items.length}</span>`;
|
|
2027
|
+
chip.style.setProperty('--chip-color', stageDef.color);
|
|
2028
|
+
chip.addEventListener('click', () => onFilter(activeFilter === stageDef.key ? null : stageDef.key));
|
|
2029
|
+
chips.appendChild(chip);
|
|
1435
2030
|
});
|
|
1436
2031
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
2032
|
+
container.innerHTML = '';
|
|
2033
|
+
container.appendChild(chips);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* Render items grouped by lifecycle stage sections.
|
|
2038
|
+
* @param {HTMLElement} listEl - Element to render sections into
|
|
2039
|
+
* @param {Object} stages - Stage classification
|
|
2040
|
+
* @param {string|null} filter - Active filter
|
|
2041
|
+
* @param {function} buildCard - Function to build a card element for an item
|
|
2042
|
+
* @param {Object} [opts] - Options like doneCollapsed
|
|
2043
|
+
*/
|
|
2044
|
+
function renderLifecycleSections(listEl, stages, filter, buildCard, opts) {
|
|
2045
|
+
const wrapper = document.createElement('div');
|
|
2046
|
+
wrapper.className = 'lifecycle-stages';
|
|
2047
|
+
const doneCollapsed = opts && opts.doneCollapsed;
|
|
2048
|
+
|
|
2049
|
+
LIFECYCLE_STAGES.forEach(stageDef => {
|
|
2050
|
+
const items = stages[stageDef.key] || [];
|
|
2051
|
+
if (filter && filter !== stageDef.key) return;
|
|
2052
|
+
if (items.length === 0 && !filter) return;
|
|
2053
|
+
|
|
2054
|
+
const isDone = stageDef.key === 'done';
|
|
2055
|
+
const isCollapsed = isDone && doneCollapsed && !filter;
|
|
2056
|
+
|
|
2057
|
+
const section = document.createElement('div');
|
|
2058
|
+
section.className = `lifecycle-section${isCollapsed ? ' collapsed' : ''}`;
|
|
2059
|
+
section.dataset.stage = stageDef.key;
|
|
2060
|
+
|
|
2061
|
+
const header = document.createElement('div');
|
|
2062
|
+
header.className = 'lifecycle-header';
|
|
2063
|
+
header.innerHTML = `
|
|
2064
|
+
<span class="lifecycle-icon" style="color:${stageDef.color}">${stageDef.icon}</span>
|
|
2065
|
+
<span class="lifecycle-label">${escHtml(stageDef.label)}</span>
|
|
2066
|
+
<span class="lifecycle-count" style="background:${stageDef.color}20;color:${stageDef.color}">${items.length}</span>
|
|
2067
|
+
${isDone ? `<span class="lifecycle-toggle">${isCollapsed ? '\u25B6' : '\u25BC'}</span>` : ''}
|
|
2068
|
+
`;
|
|
2069
|
+
if (isDone) {
|
|
2070
|
+
header.style.cursor = 'pointer';
|
|
2071
|
+
header.addEventListener('click', () => {
|
|
2072
|
+
if (opts && opts.onToggleDone) opts.onToggleDone();
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
section.appendChild(header);
|
|
2076
|
+
|
|
2077
|
+
if (!isCollapsed) {
|
|
2078
|
+
const cardsContainer = document.createElement('div');
|
|
2079
|
+
cardsContainer.className = 'drill-cards lifecycle-cards';
|
|
2080
|
+
if (opts && opts.nodeType) cardsContainer.dataset.nodeType = opts.nodeType;
|
|
2081
|
+
|
|
2082
|
+
items.forEach(item => {
|
|
2083
|
+
const el = buildCard(item);
|
|
2084
|
+
cardsContainer.appendChild(el);
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
section.appendChild(cardsContainer);
|
|
2088
|
+
wireInlineReviewButtons(cardsContainer);
|
|
2089
|
+
wireEntityMenus(cardsContainer);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
wrapper.appendChild(section);
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
listEl.appendChild(wrapper);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Empty state onboarding — shown when project has no declarations.
|
|
2100
|
+
* Renders centered project name, inline declaration input, and explanation.
|
|
2101
|
+
*/
|
|
2102
|
+
function renderEmptyOnboarding() {
|
|
2103
|
+
const projectName = (projectInfo && (projectInfo.title || projectInfo.folder)) || 'Your Project';
|
|
2104
|
+
|
|
2105
|
+
$drillList.innerHTML = `
|
|
2106
|
+
<div class="onboarding-empty">
|
|
2107
|
+
<div class="onboarding-project">${escHtml(projectName)}</div>
|
|
2108
|
+
<div class="onboarding-heading">Describe your vision</div>
|
|
2109
|
+
<div class="onboarding-desc">Tell us what you're building. We'll ask clarifying questions, then break it down into concrete declarations with milestones.</div>
|
|
2110
|
+
<div class="onboarding-form">
|
|
2111
|
+
<textarea id="onboarding-vision" class="onboarding-textarea" placeholder="Describe what you're building, what problems it solves, key features, target users..." rows="10"></textarea>
|
|
2112
|
+
<button id="onboarding-submit" class="onboarding-btn">Let's go</button>
|
|
2113
|
+
<div id="onboarding-error" class="onboarding-error"></div>
|
|
2114
|
+
</div>
|
|
2115
|
+
</div>
|
|
2116
|
+
`;
|
|
2117
|
+
|
|
2118
|
+
$drillPrompt.innerHTML = '';
|
|
2119
|
+
|
|
2120
|
+
const $vision = document.getElementById('onboarding-vision');
|
|
2121
|
+
const $btn = document.getElementById('onboarding-submit');
|
|
2122
|
+
const $err = document.getElementById('onboarding-error');
|
|
2123
|
+
|
|
2124
|
+
function submitVision() {
|
|
2125
|
+
const text = ($vision.value || '').trim();
|
|
2126
|
+
if (!text) {
|
|
2127
|
+
$err.textContent = 'Describe your vision first.';
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
startOnboard(text);
|
|
1453
2131
|
}
|
|
2132
|
+
|
|
2133
|
+
$btn.addEventListener('click', submitVision);
|
|
2134
|
+
$vision.addEventListener('keydown', (e) => {
|
|
2135
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submitVision();
|
|
2136
|
+
});
|
|
2137
|
+
requestAnimationFrame(() => $vision.focus());
|
|
1454
2138
|
}
|
|
1455
2139
|
|
|
1456
2140
|
/**
|
|
@@ -1487,58 +2171,77 @@ function renderDrillMilestones(enrichedDeclarations, enrichedMilestones, actions
|
|
|
1487
2171
|
wireInlineReviewButtons($drillDetail);
|
|
1488
2172
|
wireEntityMenus($drillDetail);
|
|
1489
2173
|
|
|
1490
|
-
$drillContext.innerHTML = '';
|
|
1491
|
-
|
|
1492
2174
|
const filtered = enrichedMilestones.filter(m => (m.realizes || []).includes(drillDeclId));
|
|
1493
2175
|
|
|
1494
2176
|
$drillList.innerHTML = '';
|
|
1495
2177
|
if (filtered.length === 0) {
|
|
2178
|
+
$drillContext.innerHTML = '';
|
|
1496
2179
|
$drillList.innerHTML = `
|
|
1497
2180
|
<div class="col-empty-invite">
|
|
1498
2181
|
<div class="empty-invite-title">No milestones yet</div>
|
|
1499
2182
|
<div class="empty-invite-desc">Plan milestones to break this declaration into achievable steps.</div>
|
|
1500
2183
|
<textarea class="empty-invite-input" id="plan-guidance-input" placeholder="Optional: guide the direction..." rows="2"></textarea>
|
|
1501
2184
|
<button class="empty-invite-btn" id="plan-milestones-btn"><kbd>P</kbd> Plan Milestones</button>
|
|
2185
|
+
<div id="derivation-progress" class="derivation-progress" style="display:none"></div>
|
|
1502
2186
|
<div id="derivation-log" class="output-log" style="display:none; min-height:0"></div>
|
|
1503
|
-
<div id="derivation-proposals" style="display:none"></div>
|
|
1504
2187
|
</div>`;
|
|
1505
2188
|
$drillPrompt.innerHTML = '';
|
|
1506
2189
|
|
|
1507
2190
|
// Wire the Plan button to trigger derivation
|
|
1508
2191
|
const planBtn = document.getElementById('plan-milestones-btn');
|
|
1509
2192
|
if (planBtn) {
|
|
1510
|
-
|
|
2193
|
+
// If derivation is already running (triggered from declaration card or restored), show active state
|
|
2194
|
+
if (derivationSessionId) {
|
|
1511
2195
|
planBtn.disabled = true;
|
|
1512
|
-
const startTime = Date.now();
|
|
1513
|
-
|
|
2196
|
+
const startTime = derivationStartTime || Date.now();
|
|
2197
|
+
const elapsed0 = Math.floor((Date.now() - startTime) / 1000);
|
|
2198
|
+
planBtn.innerHTML = '<span class="derive-spinner"></span> Planning milestones\u2026 <span class="derive-elapsed">' + fmtElapsed(elapsed0) + '</span>';
|
|
1514
2199
|
const elSpan = planBtn.querySelector('.derive-elapsed');
|
|
1515
2200
|
const timer = setInterval(() => {
|
|
1516
2201
|
if (!elSpan || !document.contains(elSpan)) { clearInterval(timer); return; }
|
|
1517
|
-
|
|
1518
|
-
elSpan.textContent = sec + 's';
|
|
2202
|
+
elSpan.textContent = fmtElapsed(Math.floor((Date.now() - startTime) / 1000));
|
|
1519
2203
|
}, 1000);
|
|
1520
2204
|
planBtn._deriveTimer = timer;
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
2205
|
+
showDerivationProgress(startTime);
|
|
2206
|
+
}
|
|
1526
2207
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
2208
|
+
planBtn.addEventListener('click', () => {
|
|
2209
|
+
planBtn.disabled = true;
|
|
2210
|
+
derivationStartTime = Date.now();
|
|
2211
|
+
const startTime = derivationStartTime;
|
|
2212
|
+
planBtn.innerHTML = '<span class="derive-spinner"></span> Planning milestones\u2026 <span class="derive-elapsed">0s</span>';
|
|
2213
|
+
const elSpan = planBtn.querySelector('.derive-elapsed');
|
|
2214
|
+
const timer = setInterval(() => {
|
|
2215
|
+
if (!elSpan || !document.contains(elSpan)) { clearInterval(timer); return; }
|
|
2216
|
+
elSpan.textContent = fmtElapsed(Math.floor((Date.now() - startTime) / 1000));
|
|
2217
|
+
}, 1000);
|
|
2218
|
+
planBtn._deriveTimer = timer;
|
|
2219
|
+
showDerivationProgress(startTime);
|
|
2220
|
+
startDerivation(drillDeclId);
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// Classify milestones into lifecycle stages
|
|
2227
|
+
const stages = classifyMilestonesToStages(filtered, actions);
|
|
2228
|
+
|
|
2229
|
+
// Render filter chips
|
|
2230
|
+
renderLifecycleFilterChipsGeneric($drillContext, stages, lifecycleFilterL2, (f) => {
|
|
2231
|
+
lifecycleFilterL2 = f;
|
|
2232
|
+
renderDrillView();
|
|
2233
|
+
});
|
|
1531
2234
|
|
|
1532
|
-
|
|
2235
|
+
// Build cards grouped by lifecycle stage
|
|
2236
|
+
let firstUnapproved = null;
|
|
2237
|
+
renderLifecycleSections($drillList, stages, lifecycleFilterL2, (m) => {
|
|
1533
2238
|
const mTitle = m.title || m.id;
|
|
1534
2239
|
const needsReview = m.reviewState !== 'approved';
|
|
1535
2240
|
const isCurrent = needsReview && !firstUnapproved;
|
|
1536
2241
|
if (needsReview && !firstUnapproved) firstUnapproved = m;
|
|
1537
2242
|
|
|
1538
2243
|
const myActions = (actions || []).filter(a => (a.causes || []).includes(m.id));
|
|
1539
|
-
|
|
1540
|
-
// Build description: milestone produces, or summarize from child action produces
|
|
1541
|
-
let mDesc = m.produces || '';
|
|
2244
|
+
let mDesc = m.description || m.produces || '';
|
|
1542
2245
|
if (!mDesc && myActions.length > 0) {
|
|
1543
2246
|
const actionDescs = myActions.map(a => a.produces).filter(Boolean);
|
|
1544
2247
|
if (actionDescs.length > 0) mDesc = actionDescs.join(' \u00B7 ');
|
|
@@ -1562,6 +2265,26 @@ function renderDrillMilestones(enrichedDeclarations, enrichedMilestones, actions
|
|
|
1562
2265
|
badgesHtml += '</span>';
|
|
1563
2266
|
}
|
|
1564
2267
|
|
|
2268
|
+
// Inline action list for milestone cards
|
|
2269
|
+
let actionListHtml = '';
|
|
2270
|
+
if (myActions.length > 0) {
|
|
2271
|
+
actionListHtml = '<ul class="drill-card-action-list">' +
|
|
2272
|
+
myActions.map(a => {
|
|
2273
|
+
const aStatus = (a.status || 'PENDING').toUpperCase();
|
|
2274
|
+
const isDone = COMPLETED.has(aStatus);
|
|
2275
|
+
const isApproved = a.reviewState === 'approved';
|
|
2276
|
+
const icon = isDone ? '\u2713' : isApproved ? '\u25CB' : '\u00B7';
|
|
2277
|
+
const cls = isDone ? 'done' : isApproved ? 'planned' : 'unplanned';
|
|
2278
|
+
return `<li class="${cls}"><span class="al-icon">${icon}</span>${escHtml(a.title || a.id)}</li>`;
|
|
2279
|
+
}).join('') + '</ul>';
|
|
2280
|
+
} else {
|
|
2281
|
+
actionListHtml = '<div class="drill-card-no-actions">No actions yet</div>';
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const runningAgent = getRunningAgentForNode(m.id);
|
|
2285
|
+
const isPending = pendingDerivations.has(m.id);
|
|
2286
|
+
const hasActiveWork = runningAgent || isPending;
|
|
2287
|
+
const activeLabel = isPending ? 'Planning…' : runningAgent ? (AGENT_TYPE_LABELS[runningAgent.type] || runningAgent.type) + '…' : '';
|
|
1565
2288
|
const el = document.createElement('div');
|
|
1566
2289
|
el.className = `drill-card${needsReview ? ' needs-review' : ''}${isCurrent ? ' current-review' : ''}`;
|
|
1567
2290
|
el.innerHTML = `
|
|
@@ -1571,38 +2294,23 @@ function renderDrillMilestones(enrichedDeclarations, enrichedMilestones, actions
|
|
|
1571
2294
|
<div class="drill-card-title">${escHtml(mTitle)}</div>
|
|
1572
2295
|
<div class="drill-card-desc">${mDesc !== mTitle ? escHtml(mDesc) : ''}</div>
|
|
1573
2296
|
<div class="drill-card-badges">${badgesHtml}</div>
|
|
2297
|
+
${actionListHtml}
|
|
1574
2298
|
</div>
|
|
1575
2299
|
</div>
|
|
1576
|
-
${renderEntityActions(m.id, m.reviewState)}
|
|
2300
|
+
${renderEntityActions(m.id, m.reviewState, undefined, hasActiveWork ? activeLabel : null)}
|
|
1577
2301
|
`;
|
|
1578
2302
|
|
|
1579
2303
|
el.addEventListener('click', (e) => {
|
|
1580
|
-
if (e.target.closest('.drill-review-btn') || e.target.closest('.drill-action-btn')) return;
|
|
2304
|
+
if (e.target.closest('.drill-review-btn') || e.target.closest('.drill-action-btn') || e.target.closest('textarea') || e.target.closest('input') || e.target.closest('.refine-container') || e.target.closest('.discuss-container')) return;
|
|
1581
2305
|
drillMileId = m.id;
|
|
1582
2306
|
drillGoDeeper('actions');
|
|
1583
2307
|
renderDrillView();
|
|
1584
2308
|
});
|
|
1585
2309
|
|
|
1586
|
-
|
|
1587
|
-
});
|
|
2310
|
+
return el;
|
|
2311
|
+
}, { nodeType: 'milestone', doneCollapsed: lifecycleDoneCollapsed, onToggleDone: () => { lifecycleDoneCollapsed = !lifecycleDoneCollapsed; renderDrillView(); } });
|
|
1588
2312
|
|
|
1589
|
-
$
|
|
1590
|
-
wireInlineReviewButtons(container);
|
|
1591
|
-
wireEntityMenus(container);
|
|
1592
|
-
|
|
1593
|
-
// Prompt
|
|
1594
|
-
if (firstUnapproved) {
|
|
1595
|
-
$drillPrompt.innerHTML = `<span class="drill-prompt-text">\u2192 Review <span class="drill-prompt-target" data-drill-target="${escHtml(firstUnapproved.id)}">${escHtml(firstUnapproved.id)}</span> to continue</span>`;
|
|
1596
|
-
$drillPrompt.querySelector('.drill-prompt-target').addEventListener('click', () => {
|
|
1597
|
-
drillMileId = firstUnapproved.id;
|
|
1598
|
-
drillLevel = 'actions';
|
|
1599
|
-
renderDrillView();
|
|
1600
|
-
});
|
|
1601
|
-
} else {
|
|
1602
|
-
$drillPrompt.innerHTML = `<span class="drill-prompt-complete">All milestones approved</span>
|
|
1603
|
-
<button class="drill-next-btn" id="drill-next-btn"><kbd>N</kbd> Next</button>`;
|
|
1604
|
-
document.getElementById('drill-next-btn').addEventListener('click', () => drillAdvanceNext());
|
|
1605
|
-
}
|
|
2313
|
+
$drillPrompt.innerHTML = '';
|
|
1606
2314
|
}
|
|
1607
2315
|
|
|
1608
2316
|
/**
|
|
@@ -1621,7 +2329,7 @@ function renderDrillActions(enrichedDeclarations, enrichedMilestones, actions) {
|
|
|
1621
2329
|
const filtered = (actions || []).filter(a => (a.causes || []).includes(drillMileId));
|
|
1622
2330
|
|
|
1623
2331
|
// Left detail panel — milestone info + review controls
|
|
1624
|
-
let produces = mile.produces || '';
|
|
2332
|
+
let produces = mile.description || mile.produces || '';
|
|
1625
2333
|
if (!produces && filtered.length > 0) {
|
|
1626
2334
|
const actionDescs = filtered.map(a => a.produces).filter(Boolean);
|
|
1627
2335
|
if (actionDescs.length > 0) produces = actionDescs.join(' · ');
|
|
@@ -1638,7 +2346,14 @@ function renderDrillActions(enrichedDeclarations, enrichedMilestones, actions) {
|
|
|
1638
2346
|
|
|
1639
2347
|
$drillDetail.classList.add('visible');
|
|
1640
2348
|
$drillDetail.dataset.nodeType = 'milestone';
|
|
2349
|
+
const declDesc = decl.statement || decl.title || '';
|
|
1641
2350
|
$drillDetail.innerHTML = `
|
|
2351
|
+
<div class="detail-parent-context">
|
|
2352
|
+
<div class="detail-id" style="opacity:0.6">${escHtml(decl.id)}</div>
|
|
2353
|
+
<div class="detail-title" style="font-size:14px;opacity:0.8">${escHtml(decl.title || '')}</div>
|
|
2354
|
+
${decl.statement ? `<div class="detail-desc" style="font-size:12px;opacity:0.6">${escHtml(truncate(decl.statement, 200))}</div>` : ''}
|
|
2355
|
+
</div>
|
|
2356
|
+
<hr style="border:none;border-top:1px solid var(--bg-3);margin:10px 0">
|
|
1642
2357
|
<div class="detail-id">${escHtml(mile.id)}</div>
|
|
1643
2358
|
<div class="detail-title">${escHtml(mileTitle)}</div>
|
|
1644
2359
|
<div class="detail-desc">${produces ? escHtml(produces) : ''}</div>
|
|
@@ -1646,29 +2361,61 @@ function renderDrillActions(enrichedDeclarations, enrichedMilestones, actions) {
|
|
|
1646
2361
|
<span class="drill-status-pill ${mileStatusClass}">${escHtml(mileStatusLabel)}</span>
|
|
1647
2362
|
${reviewBadgeHtml(mile.id, mile.reviewState)}
|
|
1648
2363
|
</div>
|
|
1649
|
-
<div class="detail-section-label">Declaration</div>
|
|
1650
|
-
<div class="detail-meta">${escHtml(decl.id)}: ${escHtml(truncate(decl.title || decl.statement || '', 60))}</div>
|
|
1651
2364
|
${renderEntityActions(mile.id, mile.reviewState, { shift: true })}
|
|
1652
2365
|
<div class="refine-container" id="refine-area-${escHtml(mile.id)}"></div>
|
|
1653
2366
|
`;
|
|
1654
2367
|
wireInlineReviewButtons($drillDetail);
|
|
1655
2368
|
wireEntityMenus($drillDetail);
|
|
1656
2369
|
|
|
1657
|
-
$drillContext.innerHTML = '';
|
|
1658
|
-
|
|
1659
2370
|
$drillList.innerHTML = '';
|
|
1660
2371
|
if (filtered.length === 0) {
|
|
1661
|
-
$
|
|
2372
|
+
$drillContext.innerHTML = '';
|
|
2373
|
+
|
|
2374
|
+
// Check if derivation is already running for this milestone
|
|
2375
|
+
const isDerivingHere = actionDerivationSessionId && actionDerivationMilestoneId === drillMileId;
|
|
2376
|
+
|
|
2377
|
+
$drillList.innerHTML = `
|
|
2378
|
+
<div class="col-empty-invite">
|
|
2379
|
+
<div class="empty-invite-title">No actions yet</div>
|
|
2380
|
+
<div class="empty-invite-desc">Plan actions to break this milestone into executable steps.</div>
|
|
2381
|
+
<textarea class="empty-invite-input" id="action-guidance-input" placeholder="Optional: guide the direction..." rows="2"></textarea>
|
|
2382
|
+
<button class="empty-invite-btn" id="action-derive-btn"${isDerivingHere ? ' disabled' : ''}>${isDerivingHere ? '<span class="derive-spinner"></span> Planning actions\u2026' : '<kbd>P</kbd> Plan Actions'}</button>
|
|
2383
|
+
<div id="action-derivation-log" class="output-log" style="${isDerivingHere ? '' : 'display:none;'} min-height:0"></div>
|
|
2384
|
+
<div id="action-derivation-proposals" style="display:none"></div>
|
|
2385
|
+
</div>`;
|
|
1662
2386
|
$drillPrompt.innerHTML = '';
|
|
2387
|
+
|
|
2388
|
+
// If derivation already has proposals, render them
|
|
2389
|
+
if (actionDerivationProposals && actionDerivationMilestoneId === drillMileId) {
|
|
2390
|
+
renderActionProposals();
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Wire the Plan button to trigger action derivation
|
|
2394
|
+
const deriveBtn = document.getElementById('action-derive-btn');
|
|
2395
|
+
if (deriveBtn && !isDerivingHere) {
|
|
2396
|
+
deriveBtn.addEventListener('click', () => {
|
|
2397
|
+
deriveBtn.disabled = true;
|
|
2398
|
+
deriveBtn.innerHTML = '<span class="derive-spinner"></span> Planning actions\u2026';
|
|
2399
|
+
const logEl = document.getElementById('action-derivation-log');
|
|
2400
|
+
if (logEl) logEl.style.display = '';
|
|
2401
|
+
startActionDerivation(drillMileId);
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
1663
2404
|
return;
|
|
1664
2405
|
}
|
|
1665
2406
|
|
|
1666
|
-
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
2407
|
+
// Classify actions into lifecycle stages
|
|
2408
|
+
const stages = classifyActionsToStages(filtered);
|
|
2409
|
+
|
|
2410
|
+
// Render filter chips
|
|
2411
|
+
renderLifecycleFilterChipsGeneric($drillContext, stages, lifecycleFilterL3, (f) => {
|
|
2412
|
+
lifecycleFilterL3 = f;
|
|
2413
|
+
renderDrillView();
|
|
2414
|
+
});
|
|
1670
2415
|
|
|
1671
|
-
|
|
2416
|
+
// Build cards grouped by lifecycle stage
|
|
2417
|
+
let firstUnapproved = null;
|
|
2418
|
+
renderLifecycleSections($drillList, stages, lifecycleFilterL3, (a) => {
|
|
1672
2419
|
const aTitle = a.title || a.id;
|
|
1673
2420
|
const aDesc = a.produces || a.title || '';
|
|
1674
2421
|
const status = a.status || 'PENDING';
|
|
@@ -1692,65 +2439,13 @@ function renderDrillActions(enrichedDeclarations, enrichedMilestones, actions) {
|
|
|
1692
2439
|
<div class="drill-card-badges">${badgesHtml}</div>
|
|
1693
2440
|
</div>
|
|
1694
2441
|
</div>
|
|
1695
|
-
<div class="drill-exec-plan-toggle" data-action-id="${escHtml(a.id)}">\u25B6 Show exec-plan</div>
|
|
1696
|
-
<div class="drill-exec-plan-content" id="drill-plan-${escHtml(a.id)}"></div>
|
|
1697
2442
|
${renderEntityActions(a.id, a.reviewState)}
|
|
1698
2443
|
`;
|
|
1699
2444
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
e.stopPropagation();
|
|
1703
|
-
const toggle = e.currentTarget;
|
|
1704
|
-
const content = el.querySelector('.drill-exec-plan-content');
|
|
1705
|
-
if (content.classList.contains('open')) {
|
|
1706
|
-
content.classList.remove('open');
|
|
1707
|
-
toggle.textContent = '\u25B6 Show exec-plan';
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
toggle.textContent = '\u25BC Loading...';
|
|
1711
|
-
try {
|
|
1712
|
-
const res = await fetch(`/api/action/${encodeURIComponent(a.id)}`);
|
|
1713
|
-
const data = await res.json();
|
|
1714
|
-
if (data.error || !data.execPlan) {
|
|
1715
|
-
content.innerHTML = '<span style="opacity:0.5">No exec-plan found</span>';
|
|
1716
|
-
} else {
|
|
1717
|
-
const ep = data.execPlan;
|
|
1718
|
-
let planHtml = '';
|
|
1719
|
-
if (ep.goal) planHtml += `<div style="margin-bottom:6px"><strong>Goal:</strong> ${escHtml(ep.goal)}</div>`;
|
|
1720
|
-
if (ep.tasks && ep.tasks.length) {
|
|
1721
|
-
planHtml += '<div style="margin-bottom:4px"><strong>Tasks:</strong></div><ol style="padding-left:16px;margin:0">';
|
|
1722
|
-
ep.tasks.forEach(t => {
|
|
1723
|
-
const taskTitle = typeof t === 'string' ? t : (t.title || t.description || JSON.stringify(t));
|
|
1724
|
-
planHtml += `<li style="margin-bottom:2px">${escHtml(truncate(taskTitle, 120))}</li>`;
|
|
1725
|
-
});
|
|
1726
|
-
planHtml += '</ol>';
|
|
1727
|
-
}
|
|
1728
|
-
if (!planHtml) planHtml = '<span style="opacity:0.5">Exec-plan loaded (no details)</span>';
|
|
1729
|
-
content.innerHTML = planHtml;
|
|
1730
|
-
}
|
|
1731
|
-
} catch (err) {
|
|
1732
|
-
content.innerHTML = `<span style="color:var(--broken-color)">Error: ${escHtml(err.message)}</span>`;
|
|
1733
|
-
}
|
|
1734
|
-
content.classList.add('open');
|
|
1735
|
-
toggle.textContent = '\u25BC Hide exec-plan';
|
|
1736
|
-
});
|
|
1737
|
-
|
|
1738
|
-
container.appendChild(el);
|
|
1739
|
-
});
|
|
1740
|
-
|
|
1741
|
-
$drillList.appendChild(container);
|
|
1742
|
-
wireInlineReviewButtons(container);
|
|
1743
|
-
wireEntityMenus(container);
|
|
2445
|
+
return el;
|
|
2446
|
+
}, { nodeType: 'action', doneCollapsed: lifecycleDoneCollapsed, onToggleDone: () => { lifecycleDoneCollapsed = !lifecycleDoneCollapsed; renderDrillView(); } });
|
|
1744
2447
|
|
|
1745
|
-
|
|
1746
|
-
const allApproved = filtered.every(a => a.reviewState === 'approved');
|
|
1747
|
-
if (allApproved) {
|
|
1748
|
-
$drillPrompt.innerHTML = `<span class="drill-prompt-complete">All actions approved</span>
|
|
1749
|
-
<button class="drill-next-btn" id="drill-next-btn"><kbd>N</kbd> Next</button>`;
|
|
1750
|
-
document.getElementById('drill-next-btn').addEventListener('click', () => drillAdvanceNext());
|
|
1751
|
-
} else if (firstUnapproved) {
|
|
1752
|
-
$drillPrompt.innerHTML = `<span class="drill-prompt-text">\u2192 Review <span class="drill-prompt-target">${escHtml(firstUnapproved.id)}</span> next</span>`;
|
|
1753
|
-
}
|
|
2448
|
+
$drillPrompt.innerHTML = '';
|
|
1754
2449
|
}
|
|
1755
2450
|
|
|
1756
2451
|
// ─── Drill helper functions ──────────────────────────────────────────────────
|
|
@@ -1769,15 +2464,96 @@ function renderInlineReviewButtons(nodeId) {
|
|
|
1769
2464
|
* Return HTML for the unified entity action menu.
|
|
1770
2465
|
* Available on ALL entities regardless of review state.
|
|
1771
2466
|
*/
|
|
1772
|
-
function renderEntityActions(nodeId, reviewState, opts) {
|
|
2467
|
+
function renderEntityActions(nodeId, reviewState, opts, activeLabel) {
|
|
1773
2468
|
const shift = opts && opts.shift;
|
|
2469
|
+
const spinnerHtml = activeLabel ? `<span class="derive-card-status"><span class="derive-spinner"></span> ${escHtml(activeLabel)}</span>` : '';
|
|
1774
2470
|
const k = (key) => shift ? `⇧${key}` : key;
|
|
1775
2471
|
const needsApprove = reviewState !== 'approved';
|
|
2472
|
+
const prefix = nodeId.split('-')[0];
|
|
2473
|
+
const isDone = graphData && (() => {
|
|
2474
|
+
const allNodes = [...(graphData.declarations || []), ...(graphData.milestones || []), ...(graphData.actions || [])];
|
|
2475
|
+
const node = allNodes.find(n => n.id === nodeId);
|
|
2476
|
+
return node && COMPLETED.has((node.status || '').toUpperCase());
|
|
2477
|
+
})();
|
|
2478
|
+
|
|
2479
|
+
if (isDone) {
|
|
2480
|
+
// Done milestones show no actions; other done nodes show Review/Delete
|
|
2481
|
+
if (prefix === 'M') return `<div class="drill-card-actions"></div>`;
|
|
2482
|
+
return `<div class="drill-card-actions">
|
|
2483
|
+
<button class="drill-action-btn" data-action="refine" data-mode="outdated" data-node-id="${escHtml(nodeId)}"><kbd>${k('R')}</kbd> Review</button>
|
|
2484
|
+
<button class="drill-action-btn drill-action-danger" data-action="delete" data-node-id="${escHtml(nodeId)}"><kbd>${k('D')}</kbd> Delete</button>
|
|
2485
|
+
</div>`;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Primary action: what this node needs most right now
|
|
2489
|
+
let primaryBtn = '';
|
|
2490
|
+
if (prefix === 'A' && graphData) {
|
|
2491
|
+
const action = (graphData.actions || []).find(a => a.id === nodeId);
|
|
2492
|
+
if (action) {
|
|
2493
|
+
const isExecuting = runningActions.has(nodeId) || EXECUTING_STATUSES_SET.has((action.status || '').toUpperCase());
|
|
2494
|
+
if (isExecuting) {
|
|
2495
|
+
primaryBtn = `<button class="drill-action-btn drill-action-primary" disabled><kbd>${k('V')}</kbd> Running...</button>`;
|
|
2496
|
+
} else if (needsApprove) {
|
|
2497
|
+
// Approve is primary — shown below
|
|
2498
|
+
} else {
|
|
2499
|
+
// Approved, not executing — Execute is primary
|
|
2500
|
+
primaryBtn = `<button class="drill-action-btn drill-action-primary" data-action="execute" data-node-id="${escHtml(nodeId)}"><kbd>${k('E')}</kbd> Execute</button>`;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
} else if (prefix === 'M' && graphData) {
|
|
2504
|
+
const milestone = (graphData.milestones || []).find(m => m.id === nodeId);
|
|
2505
|
+
const myActions = (graphData.actions || []).filter(a => (a.causes || []).includes(nodeId));
|
|
2506
|
+
if (needsApprove) {
|
|
2507
|
+
// Unapproved milestone — no primary action, just Approve/Edit/Delete (shown below)
|
|
2508
|
+
} else if (myActions.length === 0) {
|
|
2509
|
+
// Approved, no actions — Plan Actions is primary
|
|
2510
|
+
primaryBtn = `<button class="drill-action-btn drill-action-primary" data-action="derive-actions" data-node-id="${escHtml(nodeId)}"><kbd>${k('P')}</kbd> Plan Actions</button>`;
|
|
2511
|
+
} else {
|
|
2512
|
+
const allApproved = myActions.every(a => a.reviewState === 'approved');
|
|
2513
|
+
const hasUnapproved = myActions.some(a => a.reviewState !== 'approved' && !COMPLETED.has((a.status || '').toUpperCase()));
|
|
2514
|
+
if (hasUnapproved) {
|
|
2515
|
+
// Has unapproved actions — primary is to navigate to review them
|
|
2516
|
+
} else if (allApproved) {
|
|
2517
|
+
// All actions approved — Execute is primary
|
|
2518
|
+
primaryBtn = `<button class="drill-action-btn drill-action-primary" data-action="execute-milestone" data-node-id="${escHtml(nodeId)}"><kbd>${k('E')}</kbd> Execute</button>`;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
} else if (prefix === 'D' && graphData) {
|
|
2522
|
+
const myMilestones = (graphData.milestones || []).filter(m => {
|
|
2523
|
+
const realizes = Array.isArray(m.realizes) ? m.realizes : [m.realizes];
|
|
2524
|
+
return realizes.includes(nodeId);
|
|
2525
|
+
});
|
|
2526
|
+
if (myMilestones.length === 0) {
|
|
2527
|
+
// No milestones — Derive Milestones is primary
|
|
2528
|
+
primaryBtn = `<button class="drill-action-btn drill-action-primary" data-action="derive-milestones" data-node-id="${escHtml(nodeId)}"><kbd>${k('P')}</kbd> Plan Milestones</button>`;
|
|
2529
|
+
} else {
|
|
2530
|
+
const hasUnapproved = myMilestones.some(m => m.reviewState !== 'approved' && !COMPLETED.has((m.status || '').toUpperCase()));
|
|
2531
|
+
if (hasUnapproved) {
|
|
2532
|
+
// Has unapproved milestones — no extra primary button, user can drill in
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// Milestones and Declarations: primary + approve (if needed) + edit + delete
|
|
2538
|
+
if (prefix === 'M' || prefix === 'D') {
|
|
2539
|
+
const editBtn = `<button class="drill-action-btn" data-action="refine" data-mode="write" data-node-id="${escHtml(nodeId)}"><kbd>${k('E')}</kbd> Edit</button>`;
|
|
2540
|
+
const deleteBtn = `<button class="drill-action-btn drill-action-danger" data-action="delete" data-node-id="${escHtml(nodeId)}"><kbd>${k('D')}</kbd> Delete</button>`;
|
|
2541
|
+
return `<div class="drill-card-actions">
|
|
2542
|
+
${primaryBtn}
|
|
2543
|
+
${needsApprove ? `<button class="drill-review-btn approve-btn" data-review-action="approved" data-node-id="${escHtml(nodeId)}"><kbd>${k('A')}</kbd> Approve</button>` : ''}
|
|
2544
|
+
${editBtn}
|
|
2545
|
+
${deleteBtn}
|
|
2546
|
+
${spinnerHtml}
|
|
2547
|
+
</div>`;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
1776
2550
|
return `<div class="drill-card-actions">
|
|
2551
|
+
${primaryBtn}
|
|
1777
2552
|
${needsApprove ? `<button class="drill-review-btn approve-btn" data-review-action="approved" data-node-id="${escHtml(nodeId)}"><kbd>${k('A')}</kbd> Approve</button>` : ''}
|
|
1778
2553
|
<button class="drill-action-btn" data-action="refine" data-mode="outdated" data-node-id="${escHtml(nodeId)}"><kbd>${k('R')}</kbd> Review</button>
|
|
1779
2554
|
<button class="drill-action-btn" data-action="refine" data-mode="write" data-node-id="${escHtml(nodeId)}"><kbd>${k('W')}</kbd> Write</button>
|
|
1780
2555
|
<button class="drill-action-btn drill-action-danger" data-action="delete" data-node-id="${escHtml(nodeId)}"><kbd>${k('D')}</kbd> Delete</button>
|
|
2556
|
+
${spinnerHtml}
|
|
1781
2557
|
</div>`;
|
|
1782
2558
|
}
|
|
1783
2559
|
|
|
@@ -1870,6 +2646,50 @@ function wireEntityMenus($container) {
|
|
|
1870
2646
|
if (confirm(`Delete ${nodeId}? This cannot be undone.`)) {
|
|
1871
2647
|
deleteNode(nodeId);
|
|
1872
2648
|
}
|
|
2649
|
+
} else if (action === 'execute') {
|
|
2650
|
+
// Execute a single action
|
|
2651
|
+
fetch('/api/action/' + encodeURIComponent(nodeId) + '/execute', {
|
|
2652
|
+
method: 'POST',
|
|
2653
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2654
|
+
}).then(r => r.json()).then(data => {
|
|
2655
|
+
if (data.error) {
|
|
2656
|
+
alert('Execute error: ' + data.error);
|
|
2657
|
+
} else {
|
|
2658
|
+
runningActions.add(nodeId);
|
|
2659
|
+
renderDrillView();
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
} else if (action === 'execute-milestone') {
|
|
2663
|
+
// Execute all approved actions for this milestone
|
|
2664
|
+
const myActions = (graphData.actions || []).filter(a =>
|
|
2665
|
+
(a.causes || []).includes(nodeId) && a.reviewState === 'approved' && !COMPLETED.has((a.status || '').toUpperCase())
|
|
2666
|
+
);
|
|
2667
|
+
for (const a of myActions) {
|
|
2668
|
+
fetch('/api/action/' + encodeURIComponent(a.id) + '/execute', {
|
|
2669
|
+
method: 'POST',
|
|
2670
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2671
|
+
}).then(r => r.json()).then(data => {
|
|
2672
|
+
if (!data.error) {
|
|
2673
|
+
runningActions.add(a.id);
|
|
2674
|
+
renderDrillView();
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
} else if (action === 'derive-actions') {
|
|
2679
|
+
// Navigate into milestone's actions view, then derive
|
|
2680
|
+
drillMileId = nodeId;
|
|
2681
|
+
drillGoDeeper('actions');
|
|
2682
|
+
renderDrillView();
|
|
2683
|
+
triggerDerivation(nodeId);
|
|
2684
|
+
} else if (action === 'derive-milestones') {
|
|
2685
|
+
// Navigate into milestones view, then start derivation
|
|
2686
|
+
drillDeclId = nodeId;
|
|
2687
|
+
drillLevel = 'milestones';
|
|
2688
|
+
renderDrillView();
|
|
2689
|
+
triggerDerivation(nodeId);
|
|
2690
|
+
} else if (action === 'discuss') {
|
|
2691
|
+
// Optional discuss phase before derivation
|
|
2692
|
+
startDiscuss(nodeId);
|
|
1873
2693
|
}
|
|
1874
2694
|
});
|
|
1875
2695
|
});
|
|
@@ -1877,6 +2697,8 @@ function wireEntityMenus($container) {
|
|
|
1877
2697
|
|
|
1878
2698
|
/** Currently active refine node */
|
|
1879
2699
|
let refineActiveNodeId = null;
|
|
2700
|
+
/** @type {Set<string>} Active refine node IDs for concurrent support */
|
|
2701
|
+
const refineActiveNodes = new Set();
|
|
1880
2702
|
|
|
1881
2703
|
/**
|
|
1882
2704
|
* Start a refine session for a node.
|
|
@@ -1938,18 +2760,166 @@ function createRefineArea(nodeId) {
|
|
|
1938
2760
|
// Fallback: append to detail panel
|
|
1939
2761
|
const det = document.getElementById(`refine-area-${nodeId}`);
|
|
1940
2762
|
if (det) return det;
|
|
2763
|
+
// Last resort: attach to drill-detail or drill-list so output is always visible
|
|
2764
|
+
const fallbackParent = document.getElementById('drill-detail') || document.getElementById('drill-list');
|
|
2765
|
+
if (fallbackParent) {
|
|
2766
|
+
const area = document.createElement('div');
|
|
2767
|
+
area.className = 'refine-container';
|
|
2768
|
+
area.id = `refine-area-${nodeId}`;
|
|
2769
|
+
fallbackParent.appendChild(area);
|
|
2770
|
+
return area;
|
|
2771
|
+
}
|
|
1941
2772
|
return document.createElement('div'); // noop
|
|
1942
2773
|
}
|
|
1943
2774
|
|
|
2775
|
+
/**
|
|
2776
|
+
* Trigger the appropriate derivation after discuss phase completes.
|
|
2777
|
+
* @param {string} nodeId
|
|
2778
|
+
*/
|
|
2779
|
+
function triggerDerivation(nodeId) {
|
|
2780
|
+
const prefix = nodeId.split('-')[0];
|
|
2781
|
+
if (prefix === 'D') {
|
|
2782
|
+
// Immediate visual feedback via pendingDerivations
|
|
2783
|
+
pendingDerivations.add(nodeId);
|
|
2784
|
+
derivationSessionId = 'pending';
|
|
2785
|
+
derivationStartTime = Date.now();
|
|
2786
|
+
derivationProposals = null;
|
|
2787
|
+
renderDrillView();
|
|
2788
|
+
// Derive milestones — capture real sessionId for SSE handler
|
|
2789
|
+
fetch('/api/milestones/derive', {
|
|
2790
|
+
method: 'POST',
|
|
2791
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2792
|
+
body: JSON.stringify({ declarationId: nodeId }),
|
|
2793
|
+
}).then(r => r.json()).then(data => {
|
|
2794
|
+
pendingDerivations.delete(nodeId);
|
|
2795
|
+
if (data.error) {
|
|
2796
|
+
console.error('Plan milestones error:', data.error);
|
|
2797
|
+
derivationSessionId = null;
|
|
2798
|
+
derivationStartTime = null;
|
|
2799
|
+
} else if (data.sessionId) {
|
|
2800
|
+
derivationSessionId = data.sessionId;
|
|
2801
|
+
}
|
|
2802
|
+
renderDrillView();
|
|
2803
|
+
}).catch(() => {
|
|
2804
|
+
pendingDerivations.delete(nodeId);
|
|
2805
|
+
derivationSessionId = null;
|
|
2806
|
+
derivationStartTime = null;
|
|
2807
|
+
renderDrillView();
|
|
2808
|
+
});
|
|
2809
|
+
} else if (prefix === 'M') {
|
|
2810
|
+
// Immediate visual feedback via pendingDerivations
|
|
2811
|
+
pendingDerivations.add(nodeId);
|
|
2812
|
+
actionDerivationSessionId = 'pending';
|
|
2813
|
+
actionDerivationMilestoneId = nodeId;
|
|
2814
|
+
actionDerivationProposals = null;
|
|
2815
|
+
renderDrillView();
|
|
2816
|
+
// Derive actions — capture real sessionId for SSE handler
|
|
2817
|
+
fetch('/api/milestones/' + encodeURIComponent(nodeId) + '/actions/derive', {
|
|
2818
|
+
method: 'POST',
|
|
2819
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2820
|
+
}).then(r => r.json()).then(data => {
|
|
2821
|
+
pendingDerivations.delete(nodeId);
|
|
2822
|
+
if (data.error) {
|
|
2823
|
+
console.error('Plan actions error:', data.error);
|
|
2824
|
+
actionDerivationSessionId = null;
|
|
2825
|
+
actionDerivationMilestoneId = null;
|
|
2826
|
+
} else if (data.sessionId) {
|
|
2827
|
+
actionDerivationSessionId = data.sessionId;
|
|
2828
|
+
}
|
|
2829
|
+
renderDrillView();
|
|
2830
|
+
}).catch(() => {
|
|
2831
|
+
pendingDerivations.delete(nodeId);
|
|
2832
|
+
actionDerivationSessionId = null;
|
|
2833
|
+
actionDerivationMilestoneId = null;
|
|
2834
|
+
renderDrillView();
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
/** @type {string|null} Node ID with an active discuss session */
|
|
2840
|
+
let discussActiveNodeId = null;
|
|
2841
|
+
/** @type {HTMLElement|null} Detached discuss container preserved across re-renders */
|
|
2842
|
+
let discussActiveContainer = null;
|
|
2843
|
+
|
|
2844
|
+
/**
|
|
2845
|
+
* Start a discuss (interview) phase for a node.
|
|
2846
|
+
* Shows a discuss area and kicks off the discuss agent.
|
|
2847
|
+
* @param {string} nodeId
|
|
2848
|
+
*/
|
|
2849
|
+
function startDiscuss(nodeId) {
|
|
2850
|
+
discussActiveNodeId = nodeId;
|
|
2851
|
+
|
|
2852
|
+
// Create the discuss container
|
|
2853
|
+
const container = document.createElement('div');
|
|
2854
|
+
container.className = 'discuss-container';
|
|
2855
|
+
container.id = `discuss-area-${nodeId}`;
|
|
2856
|
+
discussActiveContainer = container;
|
|
2857
|
+
|
|
2858
|
+
// Attach to current card
|
|
2859
|
+
reattachDiscussContainer();
|
|
2860
|
+
|
|
2861
|
+
container.innerHTML = `<div class="discuss-loading">
|
|
2862
|
+
<pre class="discuss-streaming" id="discuss-output-${nodeId}">Thinking...</pre>
|
|
2863
|
+
<button class="drill-action-btn" id="discuss-skip-early-${nodeId}">Skip</button>
|
|
2864
|
+
</div>`;
|
|
2865
|
+
container.querySelector(`#discuss-skip-early-${nodeId}`).addEventListener('click', () => {
|
|
2866
|
+
clearDiscussState();
|
|
2867
|
+
triggerDerivation(nodeId);
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
fetch(`/api/node/${encodeURIComponent(nodeId)}/discuss`, {
|
|
2871
|
+
method: 'POST',
|
|
2872
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2873
|
+
}).then(r => r.json()).then(data => {
|
|
2874
|
+
if (data.error) {
|
|
2875
|
+
clearDiscussState();
|
|
2876
|
+
triggerDerivation(nodeId);
|
|
2877
|
+
}
|
|
2878
|
+
}).catch(() => {
|
|
2879
|
+
clearDiscussState();
|
|
2880
|
+
triggerDerivation(nodeId);
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/** Clear discuss tracking state */
|
|
2885
|
+
function clearDiscussState() {
|
|
2886
|
+
if (discussActiveContainer && discussActiveContainer.parentNode) {
|
|
2887
|
+
discussActiveContainer.parentNode.removeChild(discussActiveContainer);
|
|
2888
|
+
}
|
|
2889
|
+
discussActiveNodeId = null;
|
|
2890
|
+
discussActiveContainer = null;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/** Re-attach the discuss container to the card after a re-render */
|
|
2894
|
+
function reattachDiscussContainer() {
|
|
2895
|
+
if (!discussActiveNodeId || !discussActiveContainer) return;
|
|
2896
|
+
// Remove from old parent if still attached
|
|
2897
|
+
if (discussActiveContainer.parentNode) {
|
|
2898
|
+
discussActiveContainer.parentNode.removeChild(discussActiveContainer);
|
|
2899
|
+
}
|
|
2900
|
+
// Find the card for this node
|
|
2901
|
+
const card = document.querySelector(`.drill-card[data-node-id="${discussActiveNodeId}"]`)
|
|
2902
|
+
|| document.querySelector(`.drill-action-btn[data-node-id="${discussActiveNodeId}"]`)?.closest('.drill-card');
|
|
2903
|
+
if (card) {
|
|
2904
|
+
card.appendChild(discussActiveContainer);
|
|
2905
|
+
} else {
|
|
2906
|
+
// Fallback: append to drill-list
|
|
2907
|
+
const parent = document.getElementById('drill-list');
|
|
2908
|
+
if (parent) parent.appendChild(discussActiveContainer);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
1944
2912
|
async function doRefine(nodeId, mode, message) {
|
|
1945
2913
|
refineActiveNodeId = nodeId;
|
|
2914
|
+
refineActiveNodes.add(nodeId);
|
|
1946
2915
|
const area = document.getElementById(`refine-area-${nodeId}`) || createRefineArea(nodeId);
|
|
1947
2916
|
area.innerHTML = `<div class="refine-area"><div class="refine-streaming">Thinking...</div>
|
|
1948
2917
|
<div class="refine-actions"><button class="refine-discard" id="refine-cancel-${nodeId}">Cancel</button></div></div>`;
|
|
1949
2918
|
area.querySelector(`#refine-cancel-${nodeId}`).addEventListener('click', async () => {
|
|
1950
2919
|
try { await fetch('/api/refine/stop', { method: 'POST' }); } catch (_) {}
|
|
1951
2920
|
area.innerHTML = '';
|
|
1952
|
-
|
|
2921
|
+
refineActiveNodes.delete(nodeId);
|
|
2922
|
+
if (refineActiveNodeId === nodeId) refineActiveNodeId = null;
|
|
1953
2923
|
});
|
|
1954
2924
|
|
|
1955
2925
|
try {
|
|
@@ -1961,12 +2931,14 @@ async function doRefine(nodeId, mode, message) {
|
|
|
1961
2931
|
if (!resp.ok) {
|
|
1962
2932
|
const err = await resp.json();
|
|
1963
2933
|
area.innerHTML = `<div class="refine-area" style="border-color:var(--broken-color)">${escHtml(err.error || 'Failed')}</div>`;
|
|
1964
|
-
|
|
2934
|
+
refineActiveNodes.delete(nodeId);
|
|
2935
|
+
if (refineActiveNodeId === nodeId) refineActiveNodeId = null;
|
|
1965
2936
|
}
|
|
1966
2937
|
// Output will stream via SSE
|
|
1967
2938
|
} catch (err) {
|
|
1968
2939
|
area.innerHTML = `<div class="refine-area" style="border-color:var(--broken-color)">${escHtml(err.message)}</div>`;
|
|
1969
|
-
|
|
2940
|
+
refineActiveNodes.delete(nodeId);
|
|
2941
|
+
if (refineActiveNodeId === nodeId) refineActiveNodeId = null;
|
|
1970
2942
|
}
|
|
1971
2943
|
}
|
|
1972
2944
|
|
|
@@ -2015,6 +2987,45 @@ function updateLocalReviewState(nodeId, newState) {
|
|
|
2015
2987
|
if (node) node.reviewState = newState;
|
|
2016
2988
|
}
|
|
2017
2989
|
|
|
2990
|
+
/**
|
|
2991
|
+
* Approve all visible entities of a given type (or all types at current drill level).
|
|
2992
|
+
* Sends parallel PUT requests and updates local state for instant feedback.
|
|
2993
|
+
*/
|
|
2994
|
+
async function approveAllVisible() {
|
|
2995
|
+
if (!graphData) return;
|
|
2996
|
+
let nodes;
|
|
2997
|
+
if (drillLevel === 'declarations') {
|
|
2998
|
+
nodes = (graphData.declarations || []).filter(n => n.reviewState !== 'approved');
|
|
2999
|
+
} else if (drillLevel === 'milestones') {
|
|
3000
|
+
const filtered = (graphData.milestones || []).filter(m =>
|
|
3001
|
+
(m.realizes || []).includes(drillDeclId)
|
|
3002
|
+
);
|
|
3003
|
+
nodes = filtered.filter(n => n.reviewState !== 'approved');
|
|
3004
|
+
} else if (drillLevel === 'actions') {
|
|
3005
|
+
const filtered = (graphData.actions || []).filter(a =>
|
|
3006
|
+
(a.causes || []).includes(drillMileId)
|
|
3007
|
+
);
|
|
3008
|
+
nodes = filtered.filter(n => n.reviewState !== 'approved');
|
|
3009
|
+
} else {
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
if (nodes.length === 0) return;
|
|
3013
|
+
|
|
3014
|
+
// Fire all approve requests in parallel
|
|
3015
|
+
const promises = nodes.map(n =>
|
|
3016
|
+
fetch(`/api/node/${encodeURIComponent(n.id)}/review-state`, {
|
|
3017
|
+
method: 'PUT',
|
|
3018
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3019
|
+
body: JSON.stringify({ reviewState: 'approved' }),
|
|
3020
|
+
}).then(resp => {
|
|
3021
|
+
if (resp.ok) updateLocalReviewState(n.id, 'approved');
|
|
3022
|
+
}).catch(err => console.error(`Failed to approve ${n.id}:`, err))
|
|
3023
|
+
);
|
|
3024
|
+
await Promise.all(promises);
|
|
3025
|
+
renderDrillView();
|
|
3026
|
+
loadActivity();
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2018
3029
|
// ─── Column browser keyboard navigation ──────────────────────────────────────
|
|
2019
3030
|
|
|
2020
3031
|
// Inject kb-focus CSS rule
|
|
@@ -2096,6 +3107,8 @@ function clearColumnBrowserKbFocus() {
|
|
|
2096
3107
|
*/
|
|
2097
3108
|
function handleColumnKeydown(e) {
|
|
2098
3109
|
if (!isColumnBrowserActive()) return;
|
|
3110
|
+
// Don't intercept keys when user is typing in an input/textarea
|
|
3111
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
|
|
2099
3112
|
|
|
2100
3113
|
const key = e.key;
|
|
2101
3114
|
|
|
@@ -2152,6 +3165,21 @@ function getFocusedCard() {
|
|
|
2152
3165
|
document.addEventListener('keydown', (e) => {
|
|
2153
3166
|
if (viewMode !== 'columns') return;
|
|
2154
3167
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
3168
|
+
|
|
3169
|
+
// Ctrl+Shift+A = Approve All visible entities (Ctrl on all platforms, not Cmd)
|
|
3170
|
+
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'a') {
|
|
3171
|
+
e.preventDefault();
|
|
3172
|
+
approveAllVisible();
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// Ctrl+Shift+P = Global Plan (top-right button)
|
|
3177
|
+
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'p') {
|
|
3178
|
+
e.preventDefault();
|
|
3179
|
+
if ($executeMainBtn && !$executeMainBtn.disabled) $executeMainBtn.click();
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
2155
3183
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
2156
3184
|
|
|
2157
3185
|
const key = e.key;
|
|
@@ -2197,16 +3225,42 @@ document.addEventListener('keydown', (e) => {
|
|
|
2197
3225
|
return;
|
|
2198
3226
|
}
|
|
2199
3227
|
|
|
2200
|
-
// P = Plan
|
|
3228
|
+
// P = Plan / Derive — trigger planning on focused/current card
|
|
2201
3229
|
if (kl === 'p') {
|
|
3230
|
+
// 1. Empty-state Plan buttons (inside a view with no children)
|
|
2202
3231
|
const planBtn = document.getElementById('plan-milestones-btn');
|
|
2203
|
-
if (planBtn) { e.preventDefault(); planBtn.click(); return; }
|
|
2204
|
-
|
|
3232
|
+
if (planBtn && !planBtn.disabled) { e.preventDefault(); planBtn.click(); return; }
|
|
3233
|
+
const actionDeriveBtn = document.getElementById('action-derive-btn');
|
|
3234
|
+
if (actionDeriveBtn && !actionDeriveBtn.disabled) { e.preventDefault(); actionDeriveBtn.click(); return; }
|
|
3235
|
+
// 2. Focused or current-review card with derive button
|
|
3236
|
+
const card = getFocusedCard();
|
|
3237
|
+
if (card) {
|
|
3238
|
+
const deriveBtn = card.querySelector('[data-action="derive-actions"], [data-action="derive-milestones"]');
|
|
3239
|
+
if (deriveBtn) {
|
|
3240
|
+
e.preventDefault();
|
|
3241
|
+
const nodeId = deriveBtn.dataset.nodeId;
|
|
3242
|
+
if (nodeId) triggerDerivation(nodeId);
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
// 3. Fallback: any visible derive button
|
|
3247
|
+
const anyDeriveBtn = document.querySelector('[data-action="derive-actions"], [data-action="derive-milestones"]');
|
|
3248
|
+
if (anyDeriveBtn) {
|
|
3249
|
+
e.preventDefault();
|
|
3250
|
+
const nodeId = anyDeriveBtn.dataset.nodeId;
|
|
3251
|
+
if (nodeId) triggerDerivation(nodeId);
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
2205
3254
|
return;
|
|
2206
3255
|
}
|
|
2207
3256
|
|
|
2208
|
-
// E =
|
|
3257
|
+
// E = Edit (click first Edit button on a card) or Execute (main button)
|
|
2209
3258
|
if (kl === 'e') {
|
|
3259
|
+
// First try Edit on a card
|
|
3260
|
+
const editBtn = document.querySelector('.drill-card.current-review .drill-action-btn[data-mode="write"]')
|
|
3261
|
+
|| document.querySelector('.drill-card .drill-action-btn[data-mode="write"]');
|
|
3262
|
+
if (editBtn) { e.preventDefault(); editBtn.click(); return; }
|
|
3263
|
+
// Fallback: Execute main button
|
|
2210
3264
|
if ($executeMainBtn && !$executeMainBtn._nextTarget && !$executeMainBtn._planMode && !$executeMainBtn.disabled) {
|
|
2211
3265
|
e.preventDefault(); $executeMainBtn.click(); return;
|
|
2212
3266
|
}
|
|
@@ -2221,13 +3275,13 @@ document.addEventListener('keydown', (e) => {
|
|
|
2221
3275
|
return;
|
|
2222
3276
|
}
|
|
2223
3277
|
|
|
2224
|
-
// Shift+R/W/D/A = act on detail panel entity
|
|
2225
|
-
if (e.shiftKey && (kl === 'r' || kl === 'w' || kl === 'd' || kl === 'a')) {
|
|
3278
|
+
// Shift+R/E/W/D/A = act on detail panel entity
|
|
3279
|
+
if (e.shiftKey && (kl === 'r' || kl === 'e' || kl === 'w' || kl === 'd' || kl === 'a')) {
|
|
2226
3280
|
const detail = document.getElementById('drill-detail');
|
|
2227
3281
|
if (!detail) return;
|
|
2228
3282
|
let btn;
|
|
2229
3283
|
if (kl === 'r') btn = detail.querySelector('.drill-action-btn[data-mode="outdated"]');
|
|
2230
|
-
else if (kl === 'w') btn = detail.querySelector('.drill-action-btn[data-mode="write"]');
|
|
3284
|
+
else if (kl === 'e' || kl === 'w') btn = detail.querySelector('.drill-action-btn[data-mode="write"]');
|
|
2231
3285
|
else if (kl === 'd') btn = detail.querySelector('.drill-action-btn[data-action="delete"]');
|
|
2232
3286
|
else if (kl === 'a') btn = detail.querySelector('.drill-review-btn[data-review-action="approved"]');
|
|
2233
3287
|
if (btn) { e.preventDefault(); btn.click(); }
|
|
@@ -2261,12 +3315,18 @@ document.addEventListener('keydown', (e) => {
|
|
|
2261
3315
|
return;
|
|
2262
3316
|
}
|
|
2263
3317
|
|
|
2264
|
-
// A = Approve focused card
|
|
3318
|
+
// A = Approve focused card (fall back to detail panel approve button)
|
|
2265
3319
|
if (kl === 'a') {
|
|
2266
3320
|
const card = getFocusedCard();
|
|
2267
|
-
if (
|
|
2268
|
-
|
|
2269
|
-
|
|
3321
|
+
if (card) {
|
|
3322
|
+
const btn = card.querySelector('.drill-review-btn[data-review-action="approved"]');
|
|
3323
|
+
if (btn) { e.preventDefault(); btn.click(); return; }
|
|
3324
|
+
}
|
|
3325
|
+
const detail = document.getElementById('drill-detail');
|
|
3326
|
+
if (detail) {
|
|
3327
|
+
const btn = detail.querySelector('.drill-review-btn[data-review-action="approved"]');
|
|
3328
|
+
if (btn) { e.preventDefault(); btn.click(); }
|
|
3329
|
+
}
|
|
2270
3330
|
return;
|
|
2271
3331
|
}
|
|
2272
3332
|
});
|
|
@@ -2294,10 +3354,10 @@ document.addEventListener('click', async (e) => {
|
|
|
2294
3354
|
console.error('Failed to update review state:', err);
|
|
2295
3355
|
return;
|
|
2296
3356
|
}
|
|
2297
|
-
//
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
3357
|
+
// Update local data + re-render for instant feedback (don't rely solely on SSE)
|
|
3358
|
+
updateLocalReviewState(nodeId, nextState);
|
|
3359
|
+
renderDrillView();
|
|
3360
|
+
loadActivity();
|
|
2301
3361
|
} catch (err) {
|
|
2302
3362
|
console.error('Failed to update review state:', err);
|
|
2303
3363
|
}
|
|
@@ -3557,7 +4617,7 @@ function renderPanelChain(item, type) {
|
|
|
3557
4617
|
|
|
3558
4618
|
// Derivation panel — derive milestones for this declaration
|
|
3559
4619
|
html += `<div class="derivation-panel" id="derivation-panel">`;
|
|
3560
|
-
html += `<button class="derive-btn" id="derive-btn">
|
|
4620
|
+
html += `<button class="derive-btn" id="derive-btn">Plan Milestones</button>`;
|
|
3561
4621
|
html += `<div id="derivation-log" class="output-log" style="display:none"></div>`;
|
|
3562
4622
|
html += `<div id="derivation-proposals" style="display:none"></div>`;
|
|
3563
4623
|
html += `</div>`;
|
|
@@ -3650,7 +4710,7 @@ function renderPanelChain(item, type) {
|
|
|
3650
4710
|
if (s.type === 'milestone') {
|
|
3651
4711
|
// Action derivation panel — derive actions for this milestone
|
|
3652
4712
|
html += `<div class="derivation-panel" id="action-derivation-panel">`;
|
|
3653
|
-
html += `<button class="derive-btn" id="action-derive-btn">
|
|
4713
|
+
html += `<button class="derive-btn" id="action-derive-btn">Plan Actions</button>`;
|
|
3654
4714
|
html += `<div id="action-derivation-log" class="output-log" style="display:none"></div>`;
|
|
3655
4715
|
html += `<div id="action-derivation-proposals" style="display:none"></div>`;
|
|
3656
4716
|
html += `</div>`;
|
|
@@ -3667,8 +4727,8 @@ function renderPanelChain(item, type) {
|
|
|
3667
4727
|
|
|
3668
4728
|
if (s.type === 'action') {
|
|
3669
4729
|
// Exec-plan placeholder — filled asynchronously after render
|
|
3670
|
-
html += `<div id="
|
|
3671
|
-
<div class="detail-label" style="opacity:0.4">Loading
|
|
4730
|
+
html += `<div id="plan-detail" style="margin-top:16px">
|
|
4731
|
+
<div class="detail-label" style="opacity:0.4">Loading plan…</div>
|
|
3672
4732
|
</div>`;
|
|
3673
4733
|
}
|
|
3674
4734
|
}
|
|
@@ -3814,7 +4874,7 @@ function renderPanelChain(item, type) {
|
|
|
3814
4874
|
}
|
|
3815
4875
|
}
|
|
3816
4876
|
|
|
3817
|
-
// If an action is focused, fetch and render its
|
|
4877
|
+
// If an action is focused, fetch and render its plan
|
|
3818
4878
|
if (focusSection && focusSection.type === 'action') {
|
|
3819
4879
|
loadExecPlan(focusSection.item.id);
|
|
3820
4880
|
}
|
|
@@ -4088,7 +5148,7 @@ async function renderWorkabilityPath(nodeId, nodeType) {
|
|
|
4088
5148
|
|
|
4089
5149
|
html += `</div>`;
|
|
4090
5150
|
|
|
4091
|
-
const execPlanEl = $panelBody.querySelector('#
|
|
5151
|
+
const execPlanEl = $panelBody.querySelector('#plan-detail');
|
|
4092
5152
|
const tempDiv = document.createElement('div');
|
|
4093
5153
|
tempDiv.innerHTML = html;
|
|
4094
5154
|
const wpEl = tempDiv.firstElementChild;
|
|
@@ -4156,11 +5216,11 @@ function extractProducedFiles(summaryContent) {
|
|
|
4156
5216
|
}
|
|
4157
5217
|
|
|
4158
5218
|
/**
|
|
4159
|
-
* Fetch /api/action/:id and render the
|
|
5219
|
+
* Fetch /api/action/:id and render the plan into #plan-detail.
|
|
4160
5220
|
* @param {string} actionId
|
|
4161
5221
|
*/
|
|
4162
5222
|
async function loadExecPlan(actionId) {
|
|
4163
|
-
const container = document.getElementById('
|
|
5223
|
+
const container = document.getElementById('plan-detail');
|
|
4164
5224
|
if (!container) return;
|
|
4165
5225
|
|
|
4166
5226
|
try {
|
|
@@ -4168,7 +5228,7 @@ async function loadExecPlan(actionId) {
|
|
|
4168
5228
|
const data = await res.json();
|
|
4169
5229
|
|
|
4170
5230
|
if (data.error || !data.execPlan) {
|
|
4171
|
-
container.innerHTML = `<div class="detail-label" style="opacity:0.4">No
|
|
5231
|
+
container.innerHTML = `<div class="detail-label" style="opacity:0.4">No plan found</div>`;
|
|
4172
5232
|
return;
|
|
4173
5233
|
}
|
|
4174
5234
|
|
|
@@ -4325,7 +5385,7 @@ async function loadExecPlan(actionId) {
|
|
|
4325
5385
|
// Output log panel (hidden by default, shown when executing)
|
|
4326
5386
|
html += `<div id="output-log" class="output-log" style="display:none"></div>`;
|
|
4327
5387
|
|
|
4328
|
-
container.innerHTML = html || `<div class="detail-label" style="opacity:0.4">No
|
|
5388
|
+
container.innerHTML = html || `<div class="detail-label" style="opacity:0.4">No plan details</div>`;
|
|
4329
5389
|
|
|
4330
5390
|
// Wire button click handlers
|
|
4331
5391
|
const execBtn = document.getElementById('exec-action-btn');
|
|
@@ -4364,7 +5424,7 @@ async function loadExecPlan(actionId) {
|
|
|
4364
5424
|
}
|
|
4365
5425
|
|
|
4366
5426
|
} catch (e) {
|
|
4367
|
-
if (container) container.innerHTML = `<div class="detail-label" style="opacity:0.4">Could not load
|
|
5427
|
+
if (container) container.innerHTML = `<div class="detail-label" style="opacity:0.4">Could not load plan</div>`;
|
|
4368
5428
|
}
|
|
4369
5429
|
}
|
|
4370
5430
|
|
|
@@ -4910,6 +5970,85 @@ function handlePlayComplete(e) {
|
|
|
4910
5970
|
|
|
4911
5971
|
// ─── Derivation controls ──────────────────────────────────────────────────────
|
|
4912
5972
|
|
|
5973
|
+
/** @type {number|null} Server-reported start time for active derivation (epoch ms) */
|
|
5974
|
+
let derivationStartTime = null;
|
|
5975
|
+
/** @type {number|null} Interval ID for derivation progress animation */
|
|
5976
|
+
let derivationProgressTimer = null;
|
|
5977
|
+
|
|
5978
|
+
/** Format seconds as m:ss (e.g. 119 → "1:59") */
|
|
5979
|
+
function fmtElapsed(sec) {
|
|
5980
|
+
const m = Math.floor(sec / 60);
|
|
5981
|
+
const s = sec % 60;
|
|
5982
|
+
return m > 0 ? m + ':' + String(s).padStart(2, '0') : s + 's';
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
const DERIVATION_PHASES = [
|
|
5986
|
+
{ at: 0, icon: '\u2699', text: 'Spawning AI agent\u2026' },
|
|
5987
|
+
{ at: 3, icon: '\uD83D\uDD0D', text: 'Analyzing declaration statement\u2026' },
|
|
5988
|
+
{ at: 7, icon: '\uD83E\uDDE0', text: 'Reasoning about what must be true\u2026' },
|
|
5989
|
+
{ at: 12, icon: '\uD83C\uDFAF', text: 'Identifying key milestones\u2026' },
|
|
5990
|
+
{ at: 18, icon: '\u2696\uFE0F', text: 'Evaluating milestone boundaries\u2026' },
|
|
5991
|
+
{ at: 25, icon: '\uD83D\uDCDD', text: 'Drafting milestone proposals\u2026' },
|
|
5992
|
+
{ at: 35, icon: '\u2728', text: 'Refining and validating output\u2026' },
|
|
5993
|
+
{ at: 50, icon: '\u23F3', text: 'Almost there\u2026' },
|
|
5994
|
+
{ at: 75, icon: '\uD83D\uDD04', text: 'Still working\u2026 complex declaration' },
|
|
5995
|
+
];
|
|
5996
|
+
|
|
5997
|
+
/**
|
|
5998
|
+
* Show animated progress phases in the derivation progress area.
|
|
5999
|
+
* @param {number} startTime - Epoch ms when derivation started
|
|
6000
|
+
*/
|
|
6001
|
+
function showDerivationProgress(startTime) {
|
|
6002
|
+
if (derivationProgressTimer) clearInterval(derivationProgressTimer);
|
|
6003
|
+
|
|
6004
|
+
const el = document.getElementById('derivation-progress');
|
|
6005
|
+
if (!el) return;
|
|
6006
|
+
el.style.display = '';
|
|
6007
|
+
|
|
6008
|
+
let lastPhaseAt = -1;
|
|
6009
|
+
function update() {
|
|
6010
|
+
if (!document.contains(el)) { clearInterval(derivationProgressTimer); return; }
|
|
6011
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
6012
|
+
let phase = DERIVATION_PHASES[0];
|
|
6013
|
+
for (const p of DERIVATION_PHASES) {
|
|
6014
|
+
if (elapsed >= p.at) phase = p;
|
|
6015
|
+
}
|
|
6016
|
+
// Only update DOM when phase changes to avoid re-triggering animation
|
|
6017
|
+
if (phase.at !== lastPhaseAt) {
|
|
6018
|
+
lastPhaseAt = phase.at;
|
|
6019
|
+
el.innerHTML = `<div class="derive-phase">
|
|
6020
|
+
<span class="derive-phase-icon">${phase.icon}</span>
|
|
6021
|
+
<span class="derive-phase-text">${phase.text}</span>
|
|
6022
|
+
</div>`;
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6026
|
+
update();
|
|
6027
|
+
derivationProgressTimer = setInterval(update, 1000);
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
/** Stop the derivation progress animation. */
|
|
6031
|
+
function stopDerivationProgress() {
|
|
6032
|
+
if (derivationProgressTimer) { clearInterval(derivationProgressTimer); derivationProgressTimer = null; }
|
|
6033
|
+
const el = document.getElementById('derivation-progress');
|
|
6034
|
+
if (el) el.style.display = 'none';
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
/**
|
|
6038
|
+
* Restore running derivation state after page refresh.
|
|
6039
|
+
* Checks /api/derivation/running and restores derivationSessionId so SSE events reconnect.
|
|
6040
|
+
*/
|
|
6041
|
+
async function restoreRunningDerivations() {
|
|
6042
|
+
try {
|
|
6043
|
+
const data = await fetchJson('/api/derivation/running');
|
|
6044
|
+
if (data && data.sessions && data.sessions.length > 0) {
|
|
6045
|
+
const first = data.sessions[0];
|
|
6046
|
+
derivationSessionId = first.sessionId;
|
|
6047
|
+
derivationStartTime = first.startTime || null;
|
|
6048
|
+
}
|
|
6049
|
+
} catch (_) {}
|
|
6050
|
+
}
|
|
6051
|
+
|
|
4913
6052
|
/**
|
|
4914
6053
|
* Trigger milestone derivation for a declaration via POST /api/milestones/derive.
|
|
4915
6054
|
* @param {string} declarationId
|
|
@@ -4918,7 +6057,7 @@ async function startDerivation(declarationId) {
|
|
|
4918
6057
|
const btn = document.getElementById('derive-btn') || document.getElementById('plan-milestones-btn');
|
|
4919
6058
|
if (btn) {
|
|
4920
6059
|
btn.disabled = true;
|
|
4921
|
-
btn.textContent = '
|
|
6060
|
+
btn.textContent = 'Planning...';
|
|
4922
6061
|
}
|
|
4923
6062
|
|
|
4924
6063
|
const logEl = document.getElementById('derivation-log');
|
|
@@ -4936,7 +6075,7 @@ async function startDerivation(declarationId) {
|
|
|
4936
6075
|
|
|
4937
6076
|
if (!res.ok) {
|
|
4938
6077
|
if (logEl) { logEl.style.display = ''; logEl.textContent = 'Error: ' + (data.error || 'Failed to start derivation'); }
|
|
4939
|
-
if (btn) { btn.disabled = false; btn.textContent = btn.id === 'plan-milestones-btn' ? 'Plan Milestones' : '
|
|
6078
|
+
if (btn) { btn.disabled = false; btn.textContent = btn.id === 'plan-milestones-btn' ? 'Plan Milestones' : 'Plan Milestones'; }
|
|
4940
6079
|
return;
|
|
4941
6080
|
}
|
|
4942
6081
|
|
|
@@ -4944,42 +6083,114 @@ async function startDerivation(declarationId) {
|
|
|
4944
6083
|
derivationProposals = null;
|
|
4945
6084
|
} catch (err) {
|
|
4946
6085
|
if (logEl) { logEl.style.display = ''; logEl.textContent = 'Error: ' + err.message; }
|
|
4947
|
-
if (btn) { btn.disabled = false; btn.textContent = btn.id === 'plan-milestones-btn' ? 'Plan Milestones' : '
|
|
6086
|
+
if (btn) { btn.disabled = false; btn.textContent = btn.id === 'plan-milestones-btn' ? 'Plan Milestones' : 'Plan Milestones'; }
|
|
4948
6087
|
}
|
|
4949
6088
|
}
|
|
4950
6089
|
|
|
4951
6090
|
/**
|
|
4952
6091
|
* Handle incoming derivation-output SSE event.
|
|
6092
|
+
* Supports both single-session (detail panel) and concurrent (card-level) derivations.
|
|
4953
6093
|
* @param {MessageEvent} e
|
|
4954
6094
|
*/
|
|
4955
6095
|
function handleDerivationOutput(e) {
|
|
4956
6096
|
try {
|
|
4957
|
-
const { sessionId, text } = JSON.parse(e.data);
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
if (
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
6097
|
+
const { sessionId, declarationId, text, stream } = JSON.parse(e.data);
|
|
6098
|
+
|
|
6099
|
+
// Concurrent derive-all mode: update activeDeriveMap
|
|
6100
|
+
if (declarationId && activeDeriveMap.has(declarationId)) {
|
|
6101
|
+
// Card-level derivation — no log panel needed
|
|
6102
|
+
return;
|
|
6103
|
+
}
|
|
6104
|
+
|
|
6105
|
+
// Single-session mode: stream to progress area
|
|
6106
|
+
// Accept if pending (optimistic) or matching session
|
|
6107
|
+
if (derivationSessionId === 'pending') derivationSessionId = sessionId;
|
|
6108
|
+
if (derivationSessionId && sessionId !== derivationSessionId) return;
|
|
6109
|
+
if (!derivationSessionId) derivationSessionId = sessionId;
|
|
6110
|
+
|
|
6111
|
+
// Update progress display with real streaming content
|
|
6112
|
+
const progressEl = document.getElementById('derivation-progress');
|
|
6113
|
+
if (progressEl && stream !== 'stderr') {
|
|
6114
|
+
progressEl.style.display = '';
|
|
6115
|
+
// Stop synthetic phases — we have real output now
|
|
6116
|
+
if (derivationProgressTimer) { clearInterval(derivationProgressTimer); derivationProgressTimer = null; }
|
|
6117
|
+
|
|
6118
|
+
if (stream === 'status') {
|
|
6119
|
+
// Status messages (e.g. "Thinking...")
|
|
6120
|
+
progressEl.innerHTML = `<div class="derive-phase">
|
|
6121
|
+
<span class="derive-phase-icon">\u2699</span>
|
|
6122
|
+
<span class="derive-phase-text">${text}</span>
|
|
6123
|
+
</div>`;
|
|
6124
|
+
} else {
|
|
6125
|
+
// Streaming text — show accumulating output
|
|
6126
|
+
if (!progressEl._streamEl) {
|
|
6127
|
+
progressEl.innerHTML = `<div class="derive-phase">
|
|
6128
|
+
<span class="derive-phase-icon">\uD83D\uDCDD</span>
|
|
6129
|
+
<span class="derive-phase-text">Writing milestones\u2026</span>
|
|
6130
|
+
</div>
|
|
6131
|
+
<pre class="derive-stream-output"></pre>`;
|
|
6132
|
+
progressEl._streamEl = progressEl.querySelector('.derive-stream-output');
|
|
6133
|
+
}
|
|
6134
|
+
if (progressEl._streamEl) {
|
|
6135
|
+
progressEl._streamEl.textContent += text;
|
|
6136
|
+
progressEl._streamEl.scrollTop = progressEl._streamEl.scrollHeight;
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
4964
6140
|
} catch (_) {}
|
|
4965
6141
|
}
|
|
4966
6142
|
|
|
4967
6143
|
/**
|
|
4968
6144
|
* Handle incoming derivation-complete SSE event.
|
|
6145
|
+
* Supports both single-session (detail panel) and concurrent (card-level) derivations.
|
|
4969
6146
|
* @param {MessageEvent} e
|
|
4970
6147
|
*/
|
|
4971
6148
|
function handleDerivationComplete(e) {
|
|
4972
6149
|
try {
|
|
4973
|
-
const { sessionId, exitCode, milestones } = JSON.parse(e.data);
|
|
4974
|
-
|
|
6150
|
+
const { sessionId, declarationId, exitCode, milestones } = JSON.parse(e.data);
|
|
6151
|
+
|
|
6152
|
+
// Concurrent derive-all mode: auto-accept milestones
|
|
6153
|
+
if (declarationId && activeDeriveMap.has(declarationId)) {
|
|
6154
|
+
activeDeriveMap.delete(declarationId);
|
|
6155
|
+
|
|
6156
|
+
if (exitCode === 0 && milestones && Array.isArray(milestones)) {
|
|
6157
|
+
// Auto-accept only if declaration doesn't already have milestones
|
|
6158
|
+
const existingMilestones = graphData ? (graphData.milestones || []).filter(m => {
|
|
6159
|
+
const realizes = Array.isArray(m.realizes) ? m.realizes : [m.realizes];
|
|
6160
|
+
return realizes.includes(declarationId);
|
|
6161
|
+
}) : [];
|
|
6162
|
+
if (existingMilestones.length === 0) {
|
|
6163
|
+
const toAccept = milestones.map(m => ({
|
|
6164
|
+
title: m.title || '',
|
|
6165
|
+
description: m.description || m.reason || '',
|
|
6166
|
+
realizes: m.realizes || declarationId || '',
|
|
6167
|
+
}));
|
|
6168
|
+
fetch('/api/milestones/derive/accept', {
|
|
6169
|
+
method: 'POST',
|
|
6170
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6171
|
+
body: JSON.stringify({ milestones: toAccept }),
|
|
6172
|
+
}).catch(() => {});
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
// Re-render to update card state
|
|
6177
|
+
renderDrillView();
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
// Single-session mode
|
|
6182
|
+
if (derivationSessionId === 'pending') derivationSessionId = sessionId;
|
|
6183
|
+
if (derivationSessionId && sessionId !== derivationSessionId) return;
|
|
4975
6184
|
|
|
4976
6185
|
derivationSessionId = null;
|
|
6186
|
+
derivationStartTime = null;
|
|
6187
|
+
stopDerivationProgress();
|
|
4977
6188
|
|
|
4978
6189
|
const btn = document.getElementById('derive-btn') || document.getElementById('plan-milestones-btn');
|
|
4979
6190
|
if (btn) {
|
|
4980
6191
|
if (btn._deriveTimer) { clearInterval(btn._deriveTimer); btn._deriveTimer = null; }
|
|
4981
6192
|
btn.disabled = false;
|
|
4982
|
-
btn.innerHTML = btn.id === 'plan-milestones-btn' ? '<kbd>P</kbd> Plan Milestones' : '
|
|
6193
|
+
btn.innerHTML = btn.id === 'plan-milestones-btn' ? '<kbd>P</kbd> Plan Milestones' : 'Plan Milestones';
|
|
4983
6194
|
}
|
|
4984
6195
|
|
|
4985
6196
|
if (exitCode !== 0) {
|
|
@@ -4989,22 +6200,39 @@ function handleDerivationComplete(e) {
|
|
|
4989
6200
|
const span = document.createElement('span');
|
|
4990
6201
|
span.style.color = 'var(--broken-color)';
|
|
4991
6202
|
span.style.fontWeight = '700';
|
|
4992
|
-
span.textContent = '\
|
|
6203
|
+
span.textContent = '\nPlanning failed (exit code ' + exitCode + ')';
|
|
4993
6204
|
logEl.appendChild(span);
|
|
4994
6205
|
}
|
|
4995
6206
|
return;
|
|
4996
6207
|
}
|
|
4997
6208
|
|
|
4998
6209
|
if (milestones && Array.isArray(milestones)) {
|
|
4999
|
-
|
|
5000
|
-
|
|
6210
|
+
// Auto-accept milestones into the DAG — but only if this declaration doesn't already have milestones
|
|
6211
|
+
const declId = declarationId || drillDeclId || '';
|
|
6212
|
+
const existingMilestones = graphData ? (graphData.milestones || []).filter(m => {
|
|
6213
|
+
const realizes = Array.isArray(m.realizes) ? m.realizes : [m.realizes];
|
|
6214
|
+
return realizes.includes(declId);
|
|
6215
|
+
}) : [];
|
|
6216
|
+
if (existingMilestones.length === 0) {
|
|
6217
|
+
const toAccept = milestones.map(m => ({
|
|
6218
|
+
title: m.title || '',
|
|
6219
|
+
description: m.description || m.reason || '',
|
|
6220
|
+
realizes: m.realizes || declId || '',
|
|
6221
|
+
}));
|
|
6222
|
+
fetch('/api/milestones/derive/accept', {
|
|
6223
|
+
method: 'POST',
|
|
6224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6225
|
+
body: JSON.stringify({ milestones: toAccept }),
|
|
6226
|
+
}).catch(() => {});
|
|
6227
|
+
}
|
|
6228
|
+
// SSE change event will trigger graph reload with the new milestones as DRAFT cards
|
|
5001
6229
|
} else {
|
|
5002
6230
|
const logEl = document.getElementById('derivation-log');
|
|
5003
6231
|
if (logEl) {
|
|
5004
6232
|
const span = document.createElement('span');
|
|
5005
6233
|
span.style.color = 'var(--integrity-partial)';
|
|
5006
6234
|
span.style.fontWeight = '600';
|
|
5007
|
-
span.textContent = '\
|
|
6235
|
+
span.textContent = '\nPlanning finished but output could not be parsed. Check the log above.';
|
|
5008
6236
|
logEl.appendChild(span);
|
|
5009
6237
|
}
|
|
5010
6238
|
}
|
|
@@ -5012,93 +6240,15 @@ function handleDerivationComplete(e) {
|
|
|
5012
6240
|
}
|
|
5013
6241
|
|
|
5014
6242
|
/**
|
|
5015
|
-
*
|
|
6243
|
+
* Legacy renderProposals — no longer needed since milestones are auto-accepted.
|
|
6244
|
+
* Kept as no-op for any remaining references.
|
|
5016
6245
|
*/
|
|
5017
|
-
function renderProposals() {
|
|
5018
|
-
const container = document.getElementById('derivation-proposals');
|
|
5019
|
-
if (!container || !derivationProposals) return;
|
|
5020
|
-
container.style.display = 'block';
|
|
5021
|
-
|
|
5022
|
-
let html = '<h4 style="margin:8px 0;color:var(--text-bright);font-size:13px">Proposed Milestones</h4>';
|
|
5023
|
-
html += '<ul class="derivation-checklist">';
|
|
5024
|
-
derivationProposals.forEach((m, idx) => {
|
|
5025
|
-
html += '<li>';
|
|
5026
|
-
html += '<input type="checkbox" checked data-idx="' + idx + '">';
|
|
5027
|
-
html += '<input type="text" value="' + escHtml(m.title || '') + '" data-idx="' + idx + '">';
|
|
5028
|
-
html += '</li>';
|
|
5029
|
-
if (m.reason) {
|
|
5030
|
-
html += '<li style="border-bottom:none;padding:0"><span class="reason">' + escHtml(m.reason) + '</span></li>';
|
|
5031
|
-
}
|
|
5032
|
-
});
|
|
5033
|
-
html += '</ul>';
|
|
5034
|
-
html += '<div style="margin-top:12px">';
|
|
5035
|
-
html += '<button class="derive-accept-btn" onclick="acceptDerivation()">Accept Selected</button>';
|
|
5036
|
-
html += '<button class="derive-cancel-btn" onclick="cancelDerivation()">Cancel</button>';
|
|
5037
|
-
html += '</div>';
|
|
5038
|
-
|
|
5039
|
-
container.innerHTML = html;
|
|
5040
|
-
}
|
|
6246
|
+
function renderProposals() {}
|
|
5041
6247
|
|
|
5042
6248
|
/**
|
|
5043
|
-
* Accept
|
|
5044
|
-
* Gathers checked items, POSTs to /api/milestones/derive/accept.
|
|
6249
|
+
* Accept derivation — legacy no-op, milestones are now auto-accepted on completion.
|
|
5045
6250
|
*/
|
|
5046
|
-
async function acceptDerivation() {
|
|
5047
|
-
const container = document.getElementById('derivation-proposals');
|
|
5048
|
-
if (!container || !derivationProposals) return;
|
|
5049
|
-
|
|
5050
|
-
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
|
5051
|
-
const textInputs = container.querySelectorAll('input[type="text"]');
|
|
5052
|
-
|
|
5053
|
-
/** @type {Array<{title: string, realizes: string}>} */
|
|
5054
|
-
const selected = [];
|
|
5055
|
-
checkboxes.forEach((cb) => {
|
|
5056
|
-
if (cb.checked) {
|
|
5057
|
-
const idx = parseInt(cb.dataset.idx, 10);
|
|
5058
|
-
const titleInput = textInputs[idx];
|
|
5059
|
-
const proposal = derivationProposals[idx];
|
|
5060
|
-
if (proposal && titleInput) {
|
|
5061
|
-
selected.push({
|
|
5062
|
-
title: titleInput.value || proposal.title,
|
|
5063
|
-
realizes: proposal.realizes || '',
|
|
5064
|
-
});
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
|
-
});
|
|
5068
|
-
|
|
5069
|
-
if (selected.length === 0) {
|
|
5070
|
-
cancelDerivation();
|
|
5071
|
-
return;
|
|
5072
|
-
}
|
|
5073
|
-
|
|
5074
|
-
try {
|
|
5075
|
-
const res = await fetch('/api/milestones/derive/accept', {
|
|
5076
|
-
method: 'POST',
|
|
5077
|
-
headers: { 'Content-Type': 'application/json' },
|
|
5078
|
-
body: JSON.stringify({ milestones: selected }),
|
|
5079
|
-
});
|
|
5080
|
-
const data = await res.json();
|
|
5081
|
-
|
|
5082
|
-
if (!res.ok) {
|
|
5083
|
-
const logEl = document.getElementById('derivation-log');
|
|
5084
|
-
if (logEl) logEl.textContent += '\nError accepting: ' + (data.error || 'Unknown error');
|
|
5085
|
-
return;
|
|
5086
|
-
}
|
|
5087
|
-
|
|
5088
|
-
// Clear derivation panel and show success
|
|
5089
|
-
derivationProposals = null;
|
|
5090
|
-
const panel = document.getElementById('derivation-panel');
|
|
5091
|
-
if (panel) {
|
|
5092
|
-
panel.innerHTML = '<div style="color:var(--act-color);font-weight:600;padding:8px 0">' +
|
|
5093
|
-
selected.length + ' milestone' + (selected.length !== 1 ? 's' : '') + ' created</div>';
|
|
5094
|
-
setTimeout(() => { if (panel) panel.innerHTML = ''; }, 3000);
|
|
5095
|
-
}
|
|
5096
|
-
// SSE change event will trigger graph reload automatically
|
|
5097
|
-
} catch (err) {
|
|
5098
|
-
const logEl = document.getElementById('derivation-log');
|
|
5099
|
-
if (logEl) logEl.textContent += '\nError: ' + err.message;
|
|
5100
|
-
}
|
|
5101
|
-
}
|
|
6251
|
+
async function acceptDerivation() {}
|
|
5102
6252
|
|
|
5103
6253
|
/**
|
|
5104
6254
|
* Cancel derivation — stop if running, clear proposals and panel.
|
|
@@ -5112,15 +6262,29 @@ async function cancelDerivation() {
|
|
|
5112
6262
|
|
|
5113
6263
|
derivationSessionId = null;
|
|
5114
6264
|
derivationProposals = null;
|
|
6265
|
+
renderDrillView();
|
|
6266
|
+
}
|
|
5115
6267
|
|
|
5116
|
-
|
|
5117
|
-
|
|
6268
|
+
/**
|
|
6269
|
+
* Trigger concurrent derivation for all declarations without milestones.
|
|
6270
|
+
* Calls POST /api/declarations/derive-all and populates activeDeriveMap.
|
|
6271
|
+
*/
|
|
6272
|
+
async function triggerDeriveAll() {
|
|
6273
|
+
try {
|
|
6274
|
+
const res = await fetch('/api/declarations/derive-all', { method: 'POST' });
|
|
6275
|
+
const data = await res.json();
|
|
6276
|
+
if (!res.ok || !data.ok) return;
|
|
5118
6277
|
|
|
5119
|
-
|
|
5120
|
-
|
|
6278
|
+
// Populate activeDeriveMap from response
|
|
6279
|
+
for (const s of (data.sessions || [])) {
|
|
6280
|
+
activeDeriveMap.set(s.declarationId, { sessionId: s.sessionId, status: 'running' });
|
|
6281
|
+
}
|
|
5121
6282
|
|
|
5122
|
-
|
|
5123
|
-
|
|
6283
|
+
// Re-render to show spinners on cards
|
|
6284
|
+
renderDrillView();
|
|
6285
|
+
} catch (err) {
|
|
6286
|
+
console.error('derive-all failed:', err);
|
|
6287
|
+
}
|
|
5124
6288
|
}
|
|
5125
6289
|
|
|
5126
6290
|
/**
|
|
@@ -5194,7 +6358,7 @@ async function saveDependencies(milestoneId, dependsOn) {
|
|
|
5194
6358
|
|
|
5195
6359
|
async function startActionDerivation(milestoneId) {
|
|
5196
6360
|
const btn = document.getElementById('action-derive-btn');
|
|
5197
|
-
if (btn) { btn.disabled = true; btn.textContent = '
|
|
6361
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Planning...'; }
|
|
5198
6362
|
const logEl = document.getElementById('action-derivation-log');
|
|
5199
6363
|
if (logEl) { logEl.style.display = ''; logEl.innerHTML = ''; }
|
|
5200
6364
|
try {
|
|
@@ -5204,7 +6368,7 @@ async function startActionDerivation(milestoneId) {
|
|
|
5204
6368
|
const data = await res.json();
|
|
5205
6369
|
if (!res.ok) {
|
|
5206
6370
|
if (logEl) logEl.textContent = 'Error: ' + (data.error || 'Failed to start action derivation');
|
|
5207
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
6371
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Plan Actions'; }
|
|
5208
6372
|
return;
|
|
5209
6373
|
}
|
|
5210
6374
|
actionDerivationSessionId = data.sessionId;
|
|
@@ -5212,14 +6376,17 @@ async function startActionDerivation(milestoneId) {
|
|
|
5212
6376
|
actionDerivationProposals = null;
|
|
5213
6377
|
} catch (err) {
|
|
5214
6378
|
if (logEl) logEl.textContent = 'Error: ' + err.message;
|
|
5215
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
6379
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Plan Actions'; }
|
|
5216
6380
|
}
|
|
5217
6381
|
}
|
|
5218
6382
|
|
|
5219
6383
|
function handleActionDerivationOutput(e) {
|
|
5220
6384
|
try {
|
|
5221
6385
|
const { sessionId, text } = JSON.parse(e.data);
|
|
5222
|
-
if
|
|
6386
|
+
// Accept output if session matches OR if we haven't captured the session ID yet
|
|
6387
|
+
if (actionDerivationSessionId && sessionId !== actionDerivationSessionId) return;
|
|
6388
|
+
// Capture session ID from first output event if we missed it
|
|
6389
|
+
if (!actionDerivationSessionId) actionDerivationSessionId = sessionId;
|
|
5223
6390
|
const logEl = document.getElementById('action-derivation-log');
|
|
5224
6391
|
if (!logEl) return;
|
|
5225
6392
|
logEl.appendChild(document.createTextNode(text + '\n'));
|
|
@@ -5229,32 +6396,50 @@ function handleActionDerivationOutput(e) {
|
|
|
5229
6396
|
|
|
5230
6397
|
function handleActionDerivationComplete(e) {
|
|
5231
6398
|
try {
|
|
5232
|
-
const { sessionId, exitCode, actions } = JSON.parse(e.data);
|
|
5233
|
-
if
|
|
6399
|
+
const { sessionId, milestoneId, exitCode, actions } = JSON.parse(e.data);
|
|
6400
|
+
// Accept completion if session matches OR if we haven't captured the session ID yet
|
|
6401
|
+
if (actionDerivationSessionId && sessionId !== actionDerivationSessionId) return;
|
|
5234
6402
|
actionDerivationSessionId = null;
|
|
5235
6403
|
const btn = document.getElementById('action-derive-btn');
|
|
5236
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
6404
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Plan Actions'; }
|
|
5237
6405
|
if (exitCode !== 0) {
|
|
5238
6406
|
const logEl = document.getElementById('action-derivation-log');
|
|
5239
6407
|
if (logEl) {
|
|
5240
6408
|
const span = document.createElement('span');
|
|
5241
6409
|
span.style.color = 'var(--broken-color)';
|
|
5242
6410
|
span.style.fontWeight = '700';
|
|
5243
|
-
span.textContent = '\nAction
|
|
6411
|
+
span.textContent = '\nAction planning failed (exit code ' + exitCode + ')';
|
|
5244
6412
|
logEl.appendChild(span);
|
|
5245
6413
|
}
|
|
5246
6414
|
return;
|
|
5247
6415
|
}
|
|
5248
6416
|
if (actions && Array.isArray(actions)) {
|
|
5249
|
-
|
|
5250
|
-
|
|
6417
|
+
// Auto-accept: persist actions immediately (same as milestones), then re-render as cards
|
|
6418
|
+
const targetMilestoneId = milestoneId || actionDerivationMilestoneId;
|
|
6419
|
+
if (targetMilestoneId) {
|
|
6420
|
+
const acceptPayload = actions.map(a => ({ title: a.title, produces: a.produces || '' }));
|
|
6421
|
+
fetch('/api/milestones/' + encodeURIComponent(targetMilestoneId) + '/actions/derive/accept', {
|
|
6422
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
6423
|
+
body: JSON.stringify({ actions: acceptPayload }),
|
|
6424
|
+
}).then(r => r.json()).then(data => {
|
|
6425
|
+
actionDerivationProposals = null;
|
|
6426
|
+
actionDerivationMilestoneId = null;
|
|
6427
|
+
if (!data.error) {
|
|
6428
|
+
// Reload graph so new actions appear as cards
|
|
6429
|
+
loadData();
|
|
6430
|
+
}
|
|
6431
|
+
}).catch(() => {
|
|
6432
|
+
actionDerivationProposals = null;
|
|
6433
|
+
actionDerivationMilestoneId = null;
|
|
6434
|
+
});
|
|
6435
|
+
}
|
|
5251
6436
|
} else {
|
|
5252
6437
|
const logEl = document.getElementById('action-derivation-log');
|
|
5253
6438
|
if (logEl) {
|
|
5254
6439
|
const span = document.createElement('span');
|
|
5255
6440
|
span.style.color = 'var(--integrity-partial)';
|
|
5256
6441
|
span.style.fontWeight = '600';
|
|
5257
|
-
span.textContent = '\
|
|
6442
|
+
span.textContent = '\nPlanning finished but output could not be parsed. Check the log above.';
|
|
5258
6443
|
logEl.appendChild(span);
|
|
5259
6444
|
}
|
|
5260
6445
|
}
|
|
@@ -5343,7 +6528,7 @@ async function cancelActionDerivation() {
|
|
|
5343
6528
|
const proposals = document.getElementById('action-derivation-proposals');
|
|
5344
6529
|
if (proposals) { proposals.style.display = 'none'; proposals.innerHTML = ''; }
|
|
5345
6530
|
const btn = document.getElementById('action-derive-btn');
|
|
5346
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
6531
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Plan Actions'; }
|
|
5347
6532
|
}
|
|
5348
6533
|
|
|
5349
6534
|
const $focusHint = document.getElementById('focus-hint');
|
|
@@ -6214,11 +7399,43 @@ if ($refreshBtn) $refreshBtn.addEventListener('click', () => { loadData(); });
|
|
|
6214
7399
|
// Execute / Next main button
|
|
6215
7400
|
if ($executeMainBtn) {
|
|
6216
7401
|
$executeMainBtn.addEventListener('click', () => {
|
|
7402
|
+
// Lifecycle-aware navigation
|
|
7403
|
+
const lca = $executeMainBtn._lifecycleAction;
|
|
7404
|
+
if (lca) {
|
|
7405
|
+
if (lca.action === 'derive-milestones' && lca.targetId) {
|
|
7406
|
+
drillDeclId = lca.targetId;
|
|
7407
|
+
drillLevel = 'milestones';
|
|
7408
|
+
if (viewMode !== 'columns') switchView('columns');
|
|
7409
|
+
else renderDrillView();
|
|
7410
|
+
return;
|
|
7411
|
+
}
|
|
7412
|
+
if (lca.action === 'derive-actions' && lca.targetId && graphData) {
|
|
7413
|
+
const mile = (graphData.milestones || []).find(m => m.id === lca.targetId);
|
|
7414
|
+
if (mile && mile.realizes && mile.realizes.length) drillDeclId = mile.realizes[0];
|
|
7415
|
+
drillMileId = lca.targetId;
|
|
7416
|
+
drillLevel = 'actions';
|
|
7417
|
+
if (viewMode !== 'columns') switchView('columns');
|
|
7418
|
+
else renderDrillView();
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
if (lca.action === 'approve' && lca.targetId && graphData) {
|
|
7422
|
+
// Navigate to the node needing approval
|
|
7423
|
+
const { declarations, milestones, actions: acts } = graphData;
|
|
7424
|
+
const enrichedM = (milestones || []).map(m => ({ ...m, ...deriveMilestoneStatus(m, acts || []) }));
|
|
7425
|
+
const enrichedD = (declarations || []).map(d => ({ ...d, displayStatus: deriveDeclarationStatus(d, enrichedM) }));
|
|
7426
|
+
navigateToItem({ id: lca.targetId, type: lca.targetType || 'declaration' }, enrichedD, enrichedM, acts || []);
|
|
7427
|
+
return;
|
|
7428
|
+
}
|
|
7429
|
+
if (lca.action === 'execute') {
|
|
7430
|
+
if (canEnterExecution()) switchView('execution');
|
|
7431
|
+
return;
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
|
|
7435
|
+
// Fallback: original logic
|
|
6217
7436
|
const target = $executeMainBtn._nextTarget;
|
|
6218
7437
|
if (target) {
|
|
6219
|
-
// Navigate to first unapproved item
|
|
6220
7438
|
if (target.action) {
|
|
6221
|
-
// Find which milestone/declaration this action belongs to
|
|
6222
7439
|
const action = target.action;
|
|
6223
7440
|
const mileId = (action.causes || [])[0];
|
|
6224
7441
|
if (mileId && graphData) {
|
|
@@ -6242,7 +7459,6 @@ if ($executeMainBtn) {
|
|
|
6242
7459
|
if (viewMode !== 'columns') switchView('columns');
|
|
6243
7460
|
else renderDrillView();
|
|
6244
7461
|
} else if ($executeMainBtn._planMode) {
|
|
6245
|
-
// Navigate to first unplanned declaration's milestones view
|
|
6246
7462
|
if (graphData) {
|
|
6247
7463
|
const { declarations, milestones, actions: acts } = graphData;
|
|
6248
7464
|
const enrichedM = (milestones || []).map(m => ({ ...m, ...deriveMilestoneStatus(m, acts || []) }));
|
|
@@ -6409,6 +7625,273 @@ function fireConfetti() {
|
|
|
6409
7625
|
setTimeout(() => { cancelAnimationFrame(frame); canvas.remove(); }, 8000);
|
|
6410
7626
|
}
|
|
6411
7627
|
|
|
7628
|
+
// --- Agent activity cards ---
|
|
7629
|
+
|
|
7630
|
+
const AGENT_TYPE_ICONS = { executor: '\uD83E\uDD16', planner: '\uD83D\uDCCB', deriver: '\u26A1', researcher: '\uD83D\uDD0D', revision: '\uD83D\uDD04', command: '\u2328\uFE0F', refine: '\uD83D\uDD0D', derivation: '\u26A1', 'action-derivation': '\u26A1', default: '\u2699\uFE0F' };
|
|
7631
|
+
const AGENT_TYPE_LABELS = { derivation: 'Planning', 'action-derivation': 'Planning Actions', revision: 'Revision', execution: 'Execution', pipeline: 'Pipeline', refine: 'Refine', discuss: 'Discuss', command: 'Command' };
|
|
7632
|
+
|
|
7633
|
+
const AGENT_STATUS_LABELS = { running: 'Running', complete: 'Done', done: 'Done', failed: 'Failed', interrupted: 'Stopped' };
|
|
7634
|
+
|
|
7635
|
+
/**
|
|
7636
|
+
* Format elapsed time between two timestamps as a human-readable string.
|
|
7637
|
+
* @param {string|number} startedAt - ISO string or ms timestamp
|
|
7638
|
+
* @param {string|number|null} [completedAt] - ISO string, ms timestamp, or null (for running agents)
|
|
7639
|
+
* @returns {string} e.g. "0:05", "1:23", "1h 05m"
|
|
7640
|
+
*/
|
|
7641
|
+
function formatElapsed(startedAt, completedAt) {
|
|
7642
|
+
const start = typeof startedAt === 'string' ? new Date(startedAt).getTime() : startedAt;
|
|
7643
|
+
const end = completedAt ? (typeof completedAt === 'string' ? new Date(completedAt).getTime() : completedAt) : Date.now();
|
|
7644
|
+
const diffSec = Math.max(0, Math.floor((end - start) / 1000));
|
|
7645
|
+
if (diffSec >= 3600) {
|
|
7646
|
+
const h = Math.floor(diffSec / 3600);
|
|
7647
|
+
const m = Math.floor((diffSec % 3600) / 60);
|
|
7648
|
+
return `${h}h ${String(m).padStart(2, '0')}m`;
|
|
7649
|
+
}
|
|
7650
|
+
const m = Math.floor(diffSec / 60);
|
|
7651
|
+
const s = diffSec % 60;
|
|
7652
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
/**
|
|
7656
|
+
* Generate a human-readable completion summary for an agent.
|
|
7657
|
+
* @param {object} agent
|
|
7658
|
+
* @returns {string}
|
|
7659
|
+
*/
|
|
7660
|
+
function getAgentCompletionSummary(agent) {
|
|
7661
|
+
const result = agent.result || {};
|
|
7662
|
+
switch (agent.type) {
|
|
7663
|
+
case 'execution':
|
|
7664
|
+
return 'Executed ' + (result.actionId || agent.target);
|
|
7665
|
+
case 'derivation': {
|
|
7666
|
+
const count = result.milestones ? result.milestones.length : 0;
|
|
7667
|
+
return count > 0 ? 'Planned ' + count + ' milestone' + (count !== 1 ? 's' : '') : 'Planning complete';
|
|
7668
|
+
}
|
|
7669
|
+
case 'action-derivation': {
|
|
7670
|
+
const count = result.actionCount;
|
|
7671
|
+
const mId = result.milestoneId || agent.target;
|
|
7672
|
+
return count != null ? 'Planned ' + count + ' action' + (count !== 1 ? 's' : '') + ' for ' + mId : 'Actions planned for ' + mId;
|
|
7673
|
+
}
|
|
7674
|
+
case 'revision':
|
|
7675
|
+
return 'Revised ' + (result.nodeId || agent.target);
|
|
7676
|
+
case 'pipeline': {
|
|
7677
|
+
const c = result.completed || 0;
|
|
7678
|
+
const f = result.failed || 0;
|
|
7679
|
+
return c + ' completed' + (f > 0 ? ', ' + f + ' failed' : '');
|
|
7680
|
+
}
|
|
7681
|
+
default:
|
|
7682
|
+
return 'Completed';
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
|
|
7686
|
+
/**
|
|
7687
|
+
* Render a single agent activity card as an HTML string.
|
|
7688
|
+
* @param {{ id: string, type: string, target: string, milestoneId?: string, status: string, startedAt: string|number, updatedAt?: string|number, completedAt?: string|number, exitCode?: number, error?: string, result?: any }} agent
|
|
7689
|
+
* @returns {string} HTML string
|
|
7690
|
+
*/
|
|
7691
|
+
function renderAgentCard(agent) {
|
|
7692
|
+
const icon = AGENT_TYPE_ICONS[agent.type] || AGENT_TYPE_ICONS.default;
|
|
7693
|
+
const statusLabel = AGENT_STATUS_LABELS[agent.status] || agent.status;
|
|
7694
|
+
const elapsed = formatElapsed(agent.startedAt, agent.completedAt || null);
|
|
7695
|
+
const completedAttr = agent.completedAt ? escHtml(String(agent.completedAt)) : '';
|
|
7696
|
+
|
|
7697
|
+
let errorHtml = '';
|
|
7698
|
+
if (agent.error) {
|
|
7699
|
+
const truncated = agent.error.length > 120 ? agent.error.slice(0, 117) + '...' : agent.error;
|
|
7700
|
+
errorHtml = `<div class="agent-card-error" title="${escHtml(agent.error)}">${escHtml(truncated)}</div>`;
|
|
7701
|
+
}
|
|
7702
|
+
|
|
7703
|
+
// Completion summary for done agents
|
|
7704
|
+
let summaryHtml = '';
|
|
7705
|
+
const isDone = agent.status === 'complete' || agent.status === 'done';
|
|
7706
|
+
if (isDone) {
|
|
7707
|
+
const summary = getAgentCompletionSummary(agent);
|
|
7708
|
+
summaryHtml = `<div class="agent-card-summary">${escHtml(summary)}</div>`;
|
|
7709
|
+
}
|
|
7710
|
+
|
|
7711
|
+
// "View Result" button for done agents only (not failed)
|
|
7712
|
+
let viewResultHtml = '';
|
|
7713
|
+
if (isDone) {
|
|
7714
|
+
viewResultHtml = `<button class="agent-card-view-result" data-agent-id="${escHtml(agent.id)}">View Result</button>`;
|
|
7715
|
+
}
|
|
7716
|
+
|
|
7717
|
+
// Timer class: final for completed/failed, ticking for running
|
|
7718
|
+
const timerClass = (isDone || agent.status === 'failed') ? 'agent-card-timer agent-timer-final' : 'agent-card-timer';
|
|
7719
|
+
|
|
7720
|
+
return `<div class="agent-card status-${escHtml(agent.status)}" data-agent-id="${escHtml(agent.id)}" data-target="${escHtml(agent.target || '')}" style="cursor:pointer">
|
|
7721
|
+
<div class="agent-card-header">
|
|
7722
|
+
<span class="agent-card-icon">${icon}</span>
|
|
7723
|
+
<span class="agent-card-target">${escHtml(agent.target || '')}</span>
|
|
7724
|
+
<span class="agent-card-badge badge-${escHtml(agent.status)}">${escHtml(statusLabel)}</span>
|
|
7725
|
+
</div>
|
|
7726
|
+
<div class="agent-card-meta">
|
|
7727
|
+
<span class="agent-card-type">${escHtml(AGENT_TYPE_LABELS[agent.type] || agent.type || '')}</span>
|
|
7728
|
+
<span class="${timerClass}" data-started="${escHtml(String(agent.startedAt))}" data-completed="${completedAttr}">${elapsed}</span>
|
|
7729
|
+
</div>
|
|
7730
|
+
${errorHtml}
|
|
7731
|
+
${summaryHtml}
|
|
7732
|
+
${viewResultHtml}
|
|
7733
|
+
</div>`;
|
|
7734
|
+
}
|
|
7735
|
+
|
|
7736
|
+
let cardTimerInterval = null;
|
|
7737
|
+
|
|
7738
|
+
/** Start the 1-second interval that updates elapsed timers on running agent cards. */
|
|
7739
|
+
function startCardTimers() {
|
|
7740
|
+
if (cardTimerInterval) return;
|
|
7741
|
+
cardTimerInterval = setInterval(() => {
|
|
7742
|
+
const timers = document.querySelectorAll('.agent-card.status-running .agent-card-timer');
|
|
7743
|
+
timers.forEach(el => {
|
|
7744
|
+
const started = el.getAttribute('data-started');
|
|
7745
|
+
if (started) el.textContent = formatElapsed(started, null);
|
|
7746
|
+
});
|
|
7747
|
+
}, 1000);
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
/** Stop the card timer interval. */
|
|
7751
|
+
function stopCardTimers() {
|
|
7752
|
+
if (cardTimerInterval) {
|
|
7753
|
+
clearInterval(cardTimerInterval);
|
|
7754
|
+
cardTimerInterval = null;
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
|
|
7758
|
+
/** Map of active/recent agent states keyed by agent ID. */
|
|
7759
|
+
const agentCardState = new Map();
|
|
7760
|
+
|
|
7761
|
+
// DOM references for card containers (A-124)
|
|
7762
|
+
const $activityCards = document.getElementById('activity-cards');
|
|
7763
|
+
const $activityCardsActive = document.getElementById('activity-cards-active');
|
|
7764
|
+
const $activityCardsRecent = document.getElementById('activity-cards-recent');
|
|
7765
|
+
|
|
7766
|
+
/**
|
|
7767
|
+
* Re-render the agent cards panel.
|
|
7768
|
+
* Splits agents from agentCardState into active (running) and recent (done/failed),
|
|
7769
|
+
* renders them into #activity-cards-active and #activity-cards-recent.
|
|
7770
|
+
*/
|
|
7771
|
+
function renderAgentPanel() {
|
|
7772
|
+
if (!$activityCardsActive) return;
|
|
7773
|
+
|
|
7774
|
+
const agents = Array.from(agentCardState.values());
|
|
7775
|
+
const active = agents
|
|
7776
|
+
.filter(a => a.status === 'running')
|
|
7777
|
+
.sort((a, b) => new Date(b.startedAt || 0).getTime() - new Date(a.startedAt || 0).getTime());
|
|
7778
|
+
const recent = agents
|
|
7779
|
+
.filter(a => a.status === 'complete' || a.status === 'done' || a.status === 'failed' || a.status === 'interrupted')
|
|
7780
|
+
.sort((a, b) => new Date(b.completedAt || b.updatedAt || 0).getTime() - new Date(a.completedAt || a.updatedAt || 0).getTime())
|
|
7781
|
+
.slice(0, 10);
|
|
7782
|
+
|
|
7783
|
+
if (active.length === 0 && recent.length === 0) {
|
|
7784
|
+
$activityCardsActive.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:11px;text-align:center;">No active agents</div>';
|
|
7785
|
+
$activityCardsRecent.innerHTML = '';
|
|
7786
|
+
stopCardTimers();
|
|
7787
|
+
return;
|
|
7788
|
+
}
|
|
7789
|
+
|
|
7790
|
+
$activityCardsActive.innerHTML = active.length > 0
|
|
7791
|
+
? active.map(renderAgentCard).join('')
|
|
7792
|
+
: '<div style="padding:16px;color:var(--text-muted);font-size:11px;text-align:center;">No active agents</div>';
|
|
7793
|
+
$activityCardsRecent.innerHTML = recent.map(renderAgentCard).join('');
|
|
7794
|
+
|
|
7795
|
+
if (active.length > 0) startCardTimers(); else stopCardTimers();
|
|
7796
|
+
}
|
|
7797
|
+
|
|
7798
|
+
// Delegated click handler for "View Result" buttons on agent cards (A-128)
|
|
7799
|
+
function handleViewResultClick(e) {
|
|
7800
|
+
const btn = e.target.closest('.agent-card-view-result');
|
|
7801
|
+
if (!btn) return;
|
|
7802
|
+
e.stopPropagation();
|
|
7803
|
+
const agentId = btn.getAttribute('data-agent-id');
|
|
7804
|
+
const agent = agentCardState.get(agentId);
|
|
7805
|
+
if (agent) navigateToResult(agent);
|
|
7806
|
+
}
|
|
7807
|
+
// Delegated click handler: clicking an agent card navigates to its target node
|
|
7808
|
+
function handleAgentCardClick(e) {
|
|
7809
|
+
// Don't navigate if clicking a button (View Result, etc.)
|
|
7810
|
+
if (e.target.closest('button')) return;
|
|
7811
|
+
const card = e.target.closest('.agent-card');
|
|
7812
|
+
if (!card) return;
|
|
7813
|
+
const target = card.getAttribute('data-target');
|
|
7814
|
+
if (!target || !graphData) return;
|
|
7815
|
+
const prefix = target.split('-')[0];
|
|
7816
|
+
if (prefix === 'D') {
|
|
7817
|
+
drillDeclId = target;
|
|
7818
|
+
drillLevel = 'milestones';
|
|
7819
|
+
drillMileId = null;
|
|
7820
|
+
pushDrillHash();
|
|
7821
|
+
renderDrillView();
|
|
7822
|
+
} else if (prefix === 'M') {
|
|
7823
|
+
const mile = (graphData.milestones || []).find(m => m.id === target);
|
|
7824
|
+
if (mile && mile.realizes && mile.realizes.length) drillDeclId = mile.realizes[0];
|
|
7825
|
+
drillMileId = target;
|
|
7826
|
+
drillLevel = 'actions';
|
|
7827
|
+
pushDrillHash();
|
|
7828
|
+
renderDrillView();
|
|
7829
|
+
} else if (prefix === 'A') {
|
|
7830
|
+
const action = (graphData.actions || []).find(a => a.id === target);
|
|
7831
|
+
if (action && action.causes && action.causes.length) {
|
|
7832
|
+
drillMileId = action.causes[0];
|
|
7833
|
+
const parentMile = (graphData.milestones || []).find(m => m.id === drillMileId);
|
|
7834
|
+
if (parentMile && parentMile.realizes && parentMile.realizes.length) drillDeclId = parentMile.realizes[0];
|
|
7835
|
+
drillLevel = 'actions';
|
|
7836
|
+
pushDrillHash();
|
|
7837
|
+
renderDrillView();
|
|
7838
|
+
}
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7841
|
+
if ($activityCardsActive) $activityCardsActive.addEventListener('click', handleAgentCardClick);
|
|
7842
|
+
if ($activityCardsRecent) $activityCardsRecent.addEventListener('click', handleAgentCardClick);
|
|
7843
|
+
|
|
7844
|
+
if ($activityCardsActive) $activityCardsActive.addEventListener('click', handleViewResultClick);
|
|
7845
|
+
if ($activityCardsRecent) $activityCardsRecent.addEventListener('click', handleViewResultClick);
|
|
7846
|
+
|
|
7847
|
+
// Tab switching for Agents/Log tabs (A-124)
|
|
7848
|
+
document.querySelectorAll('.activity-tab').forEach(tab => {
|
|
7849
|
+
tab.addEventListener('click', () => {
|
|
7850
|
+
document.querySelectorAll('.activity-tab').forEach(t => t.classList.remove('active'));
|
|
7851
|
+
document.querySelectorAll('.activity-tab-content').forEach(c => c.classList.remove('active'));
|
|
7852
|
+
tab.classList.add('active');
|
|
7853
|
+
const target = tab.dataset.tab === 'cards' ? $activityCards : document.getElementById('activity-list');
|
|
7854
|
+
if (target) target.classList.add('active');
|
|
7855
|
+
});
|
|
7856
|
+
});
|
|
7857
|
+
|
|
7858
|
+
/**
|
|
7859
|
+
* Check if any agent is actively running for a given node ID.
|
|
7860
|
+
* @param {string} nodeId
|
|
7861
|
+
* @returns {{ type: string, id: string } | null}
|
|
7862
|
+
*/
|
|
7863
|
+
function getRunningAgentForNode(nodeId) {
|
|
7864
|
+
for (const [, agent] of agentCardState) {
|
|
7865
|
+
if (agent.status === 'running' && agent.target === nodeId) return agent;
|
|
7866
|
+
}
|
|
7867
|
+
return null;
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
/**
|
|
7871
|
+
* Fetch current agent state from server and populate card state.
|
|
7872
|
+
* Called on page load and SSE reconnect to ensure cards survive refresh.
|
|
7873
|
+
*/
|
|
7874
|
+
async function loadAgentCards() {
|
|
7875
|
+
try {
|
|
7876
|
+
const res = await fetch('/api/agents');
|
|
7877
|
+
if (!res.ok) return;
|
|
7878
|
+
const data = await res.json();
|
|
7879
|
+
const agents = [].concat(data.active || [], data.recent || []);
|
|
7880
|
+
|
|
7881
|
+
// Replace entire state with server truth
|
|
7882
|
+
agentCardState.clear();
|
|
7883
|
+
for (const agent of agents) {
|
|
7884
|
+
agentCardState.set(agent.id, agent);
|
|
7885
|
+
}
|
|
7886
|
+
renderAgentPanel();
|
|
7887
|
+
if (agents.length > 0) {
|
|
7888
|
+
console.log('[agents]', agents.length, 'agents loaded, active:', agents.filter(a => a.status === 'running').length);
|
|
7889
|
+
}
|
|
7890
|
+
} catch (err) {
|
|
7891
|
+
console.error('[agents] loadAgentCards failed:', err);
|
|
7892
|
+
}
|
|
7893
|
+
}
|
|
7894
|
+
|
|
6412
7895
|
// ─── Activity topbar ──────────────────────────────────────────────────────────
|
|
6413
7896
|
|
|
6414
7897
|
/** @type {{ actionId: string, milestoneId?: string, startedAt: number } | null} */
|
|
@@ -6416,16 +7899,8 @@ let topbarActiveOp = null;
|
|
|
6416
7899
|
/** @type {{ actionId: string, milestoneId?: string, completedAt: number } | null} */
|
|
6417
7900
|
let topbarLastOp = null;
|
|
6418
7901
|
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
if (topbarActiveOp) {
|
|
6422
|
-
const aLabel = topbarActiveOp.actionId;
|
|
6423
|
-
const mLabel = topbarActiveOp.milestoneId ? ' \u2014 ' + topbarActiveOp.milestoneId : '';
|
|
6424
|
-
$activityPinned.innerHTML = '<div class="activity-pinned"><span class="pinned-spinner"></span> EXECUTING ' + escHtml(aLabel) + escHtml(mLabel) + '</div>';
|
|
6425
|
-
} else {
|
|
6426
|
-
$activityPinned.innerHTML = '';
|
|
6427
|
-
}
|
|
6428
|
-
}
|
|
7902
|
+
// updateTopbar() — no-op, replaced by agent cards panel (A-124)
|
|
7903
|
+
function updateTopbar() {}
|
|
6429
7904
|
|
|
6430
7905
|
function formatTimeAgo(ts) {
|
|
6431
7906
|
var diff = Math.floor((Date.now() - ts) / 1000);
|
|
@@ -6459,25 +7934,13 @@ function topbarOnActionComplete(actionId) {
|
|
|
6459
7934
|
updateTopbar();
|
|
6460
7935
|
}
|
|
6461
7936
|
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
var actionMatch = (taskStart.desc || '').match(/A-\d+/i);
|
|
6470
|
-
var mileMatch = (taskStart.desc || '').match(/M-\d+/i);
|
|
6471
|
-
if (actionMatch) {
|
|
6472
|
-
topbarActiveOp = {
|
|
6473
|
-
actionId: actionMatch[0].toUpperCase(),
|
|
6474
|
-
milestoneId: mileMatch ? mileMatch[0].toUpperCase() : '',
|
|
6475
|
-
startedAt: new Date(taskStart.ts).getTime() || Date.now(),
|
|
6476
|
-
};
|
|
6477
|
-
updateTopbar();
|
|
6478
|
-
}
|
|
6479
|
-
}
|
|
6480
|
-
} catch (_) {}
|
|
7937
|
+
// topbarOnActivity() — simplified to just pulse the activity indicator (A-124)
|
|
7938
|
+
function topbarOnActivity() {
|
|
7939
|
+
if ($activityPulse) {
|
|
7940
|
+
$activityPulse.classList.add('live');
|
|
7941
|
+
clearTimeout($activityPulse._topbarTimer);
|
|
7942
|
+
$activityPulse._topbarTimer = setTimeout(() => $activityPulse.classList.remove('live'), 3000);
|
|
7943
|
+
}
|
|
6481
7944
|
}
|
|
6482
7945
|
|
|
6483
7946
|
setInterval(function() { if (!topbarActiveOp && topbarLastOp) updateTopbar(); }, 30000);
|
|
@@ -6535,7 +7998,12 @@ async function loadActivity() {
|
|
|
6535
7998
|
return '';
|
|
6536
7999
|
}
|
|
6537
8000
|
|
|
6538
|
-
|
|
8001
|
+
// Extract node ID from desc for navigation
|
|
8002
|
+
const nodeMatch = (desc || '').match(/\b([DMA]-\d+)\b/);
|
|
8003
|
+
const clickable = nodeMatch ? ' clickable' : '';
|
|
8004
|
+
const nodeAttr = nodeMatch ? ` data-nav-node="${nodeMatch[1]}"` : '';
|
|
8005
|
+
|
|
8006
|
+
return `<div class="activity-event${clickable}"${nodeAttr}>
|
|
6539
8007
|
<div class="ae-top">
|
|
6540
8008
|
<span class="ae-icon">${icon}</span>
|
|
6541
8009
|
<span class="ae-label ${labelClass}">${escHtml(label)}</span>
|
|
@@ -6568,8 +8036,8 @@ const STATE_DISPLAY = {
|
|
|
6568
8036
|
|
|
6569
8037
|
const STATE_BTN_LABEL = {
|
|
6570
8038
|
'create-declaration': '+ Declaration',
|
|
6571
|
-
'derive-milestones': '
|
|
6572
|
-
'derive-actions': '
|
|
8039
|
+
'derive-milestones': 'Plan Milestones',
|
|
8040
|
+
'derive-actions': 'Plan Actions',
|
|
6573
8041
|
'execute-action': 'Execute',
|
|
6574
8042
|
'plan-actions': 'Plan Actions',
|
|
6575
8043
|
'view-execution': 'View Execution',
|
|
@@ -6590,6 +8058,18 @@ async function loadWorkflowState() {
|
|
|
6590
8058
|
}
|
|
6591
8059
|
}
|
|
6592
8060
|
|
|
8061
|
+
async function loadLifecycleData() {
|
|
8062
|
+
try {
|
|
8063
|
+
lifecycleData = await fetchJson('/api/lifecycle');
|
|
8064
|
+
// Re-render if we're at the declarations level (lifecycle view)
|
|
8065
|
+
if (drillLevel === 'declarations' && viewMode === 'columns') {
|
|
8066
|
+
renderDrillView();
|
|
8067
|
+
}
|
|
8068
|
+
} catch (_) {
|
|
8069
|
+
lifecycleData = null;
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
|
|
6593
8073
|
/**
|
|
6594
8074
|
* Render the workflow next-step banner from current workflowState.
|
|
6595
8075
|
*/
|
|
@@ -6667,9 +8147,45 @@ if ($wfActionBtn) {
|
|
|
6667
8147
|
|
|
6668
8148
|
function connectSSE() {
|
|
6669
8149
|
const es = new EventSource('/events');
|
|
8150
|
+
es.addEventListener('open', function() {
|
|
8151
|
+
sseReconnectDelay = 1000;
|
|
8152
|
+
hideReconnectBanner();
|
|
8153
|
+
// Re-sync agent state on reconnect
|
|
8154
|
+
loadAgentCards();
|
|
8155
|
+
});
|
|
8156
|
+
let sseChangeTimer = null;
|
|
6670
8157
|
es.addEventListener('change', () => {
|
|
6671
8158
|
if (focusNodeId || focusCleanupTimer) return; // skip during animation
|
|
6672
|
-
|
|
8159
|
+
// Debounce rapid SSE change events (e.g. approve writes multiple files)
|
|
8160
|
+
clearTimeout(sseChangeTimer);
|
|
8161
|
+
sseChangeTimer = setTimeout(() => {
|
|
8162
|
+
// If a refine or discuss session is active, save and restore the area across re-renders
|
|
8163
|
+
if (refineActiveNodeId || refineActiveNodes.size > 0 || discussActiveNodeId) {
|
|
8164
|
+
// Save refine area content before re-render
|
|
8165
|
+
let savedRefineHtml = null;
|
|
8166
|
+
const savedRefineNodeId = refineActiveNodeId;
|
|
8167
|
+
if (savedRefineNodeId) {
|
|
8168
|
+
const existingArea = document.getElementById(`refine-area-${savedRefineNodeId}`);
|
|
8169
|
+
savedRefineHtml = existingArea ? existingArea.innerHTML : null;
|
|
8170
|
+
}
|
|
8171
|
+
// Save discuss container (detached element — just need to re-attach after render)
|
|
8172
|
+
const savedDiscussNodeId = discussActiveNodeId;
|
|
8173
|
+
loadData().then(() => {
|
|
8174
|
+
// Restore refine area
|
|
8175
|
+
if (savedRefineHtml && savedRefineNodeId) {
|
|
8176
|
+
const restoredArea = document.getElementById(`refine-area-${savedRefineNodeId}`);
|
|
8177
|
+
if (restoredArea) restoredArea.innerHTML = savedRefineHtml;
|
|
8178
|
+
}
|
|
8179
|
+
// Re-attach discuss container
|
|
8180
|
+
if (savedDiscussNodeId && discussActiveContainer) {
|
|
8181
|
+
reattachDiscussContainer();
|
|
8182
|
+
}
|
|
8183
|
+
});
|
|
8184
|
+
} else {
|
|
8185
|
+
loadData();
|
|
8186
|
+
}
|
|
8187
|
+
loadAgentCards(); // refresh agent cards on any .planning/ change
|
|
8188
|
+
}, 300);
|
|
6673
8189
|
});
|
|
6674
8190
|
es.addEventListener('activity', () => {
|
|
6675
8191
|
loadActivity();
|
|
@@ -6741,8 +8257,10 @@ function connectSSE() {
|
|
|
6741
8257
|
es.addEventListener('refine-output', function(e) {
|
|
6742
8258
|
try {
|
|
6743
8259
|
const data = JSON.parse(e.data);
|
|
6744
|
-
|
|
6745
|
-
const
|
|
8260
|
+
// Find matching active refine by nodeId
|
|
8261
|
+
const nId = [...refineActiveNodes].find(id => id.toUpperCase() === data.nodeId) || refineActiveNodeId;
|
|
8262
|
+
if (!nId) return;
|
|
8263
|
+
const area = document.getElementById(`refine-area-${nId}`);
|
|
6746
8264
|
if (!area) return;
|
|
6747
8265
|
const streaming = area.querySelector('.refine-streaming');
|
|
6748
8266
|
if (streaming) {
|
|
@@ -6754,9 +8272,10 @@ function connectSSE() {
|
|
|
6754
8272
|
es.addEventListener('refine-complete', function(e) {
|
|
6755
8273
|
try {
|
|
6756
8274
|
const data = JSON.parse(e.data);
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
8275
|
+
const nId = [...refineActiveNodes].find(id => id.toUpperCase() === data.nodeId) || refineActiveNodeId;
|
|
8276
|
+
if (!nId) return;
|
|
8277
|
+
refineActiveNodes.delete(nId);
|
|
8278
|
+
if (refineActiveNodeId === nId) refineActiveNodeId = null;
|
|
6760
8279
|
const area = document.getElementById(`refine-area-${nId}`);
|
|
6761
8280
|
if (!area) return;
|
|
6762
8281
|
|
|
@@ -6810,17 +8329,689 @@ function connectSSE() {
|
|
|
6810
8329
|
}
|
|
6811
8330
|
} catch (_) {}
|
|
6812
8331
|
});
|
|
8332
|
+
// ─── Discuss (interview) SSE events ──────────────────────────────────────
|
|
8333
|
+
es.addEventListener('discuss-output', function(e) {
|
|
8334
|
+
try {
|
|
8335
|
+
const data = JSON.parse(e.data);
|
|
8336
|
+
const outputEl = document.getElementById(`discuss-output-${data.nodeId}`);
|
|
8337
|
+
if (outputEl) {
|
|
8338
|
+
if (outputEl.textContent === 'Thinking...') outputEl.textContent = '';
|
|
8339
|
+
outputEl.textContent += data.text;
|
|
8340
|
+
}
|
|
8341
|
+
} catch (_) {}
|
|
8342
|
+
});
|
|
8343
|
+
es.addEventListener('discuss-complete', function(e) {
|
|
8344
|
+
try {
|
|
8345
|
+
const data = JSON.parse(e.data);
|
|
8346
|
+
const container = document.getElementById(`discuss-area-${data.nodeId}`);
|
|
8347
|
+
if (!container) return;
|
|
8348
|
+
if (data.error || !Array.isArray(data.questions) || data.questions.length === 0) {
|
|
8349
|
+
container.innerHTML = `<div class="discuss-result">
|
|
8350
|
+
<div class="discuss-error">${escHtml(data.error || 'No questions generated. Proceed with derivation.')}</div>
|
|
8351
|
+
<button class="drill-action-btn" id="discuss-skip-${data.nodeId}">Proceed</button>
|
|
8352
|
+
</div>`;
|
|
8353
|
+
container.querySelector(`#discuss-skip-${data.nodeId}`).addEventListener('click', () => {
|
|
8354
|
+
clearDiscussState();
|
|
8355
|
+
triggerDerivation(data.nodeId);
|
|
8356
|
+
});
|
|
8357
|
+
return;
|
|
8358
|
+
}
|
|
8359
|
+
// Show questions as a form
|
|
8360
|
+
let html = '<div class="discuss-questions">';
|
|
8361
|
+
html += '<div class="discuss-header">Answer these questions to improve the plan:</div>';
|
|
8362
|
+
data.questions.forEach((q, i) => {
|
|
8363
|
+
html += `<div class="discuss-q">
|
|
8364
|
+
<label class="discuss-q-label">${escHtml(q.question)}</label>
|
|
8365
|
+
${q.context ? `<div class="discuss-q-context">${escHtml(q.context)}</div>` : ''}
|
|
8366
|
+
${q.options && q.options.length > 0
|
|
8367
|
+
? `<div class="discuss-q-options">${q.options.map(opt =>
|
|
8368
|
+
`<button class="discuss-option-btn" data-q="${i}" data-opt="${escHtml(opt)}">${escHtml(opt)}</button>`
|
|
8369
|
+
).join('')}</div>`
|
|
8370
|
+
: ''}
|
|
8371
|
+
<textarea class="discuss-q-input" data-q="${i}" rows="2" placeholder="Your answer..."></textarea>
|
|
8372
|
+
</div>`;
|
|
8373
|
+
});
|
|
8374
|
+
html += `<div class="discuss-actions">
|
|
8375
|
+
<button class="drill-action-btn drill-action-primary" id="discuss-submit-${data.nodeId}">Save & Proceed</button>
|
|
8376
|
+
<button class="drill-action-btn" id="discuss-skip-${data.nodeId}">Skip</button>
|
|
8377
|
+
</div></div>`;
|
|
8378
|
+
container.innerHTML = html;
|
|
8379
|
+
|
|
8380
|
+
// Wire option buttons to fill textarea
|
|
8381
|
+
container.querySelectorAll('.discuss-option-btn').forEach(btn => {
|
|
8382
|
+
btn.addEventListener('click', () => {
|
|
8383
|
+
const idx = btn.dataset.q;
|
|
8384
|
+
const ta = container.querySelector(`.discuss-q-input[data-q="${idx}"]`);
|
|
8385
|
+
if (ta) ta.value = btn.dataset.opt;
|
|
8386
|
+
});
|
|
8387
|
+
});
|
|
8388
|
+
|
|
8389
|
+
// Wire submit
|
|
8390
|
+
container.querySelector(`#discuss-submit-${data.nodeId}`).addEventListener('click', async () => {
|
|
8391
|
+
const answers = [];
|
|
8392
|
+
data.questions.forEach((q, i) => {
|
|
8393
|
+
const ta = container.querySelector(`.discuss-q-input[data-q="${i}"]`);
|
|
8394
|
+
answers.push({ question: q.question, answer: ta ? ta.value.trim() : '' });
|
|
8395
|
+
});
|
|
8396
|
+
const filtered = answers.filter(a => a.answer);
|
|
8397
|
+
if (filtered.length > 0) {
|
|
8398
|
+
await fetch(`/api/node/${encodeURIComponent(data.nodeId)}/discuss/answer`, {
|
|
8399
|
+
method: 'POST',
|
|
8400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8401
|
+
body: JSON.stringify({ answers: filtered }),
|
|
8402
|
+
});
|
|
8403
|
+
}
|
|
8404
|
+
clearDiscussState();
|
|
8405
|
+
triggerDerivation(data.nodeId);
|
|
8406
|
+
});
|
|
8407
|
+
|
|
8408
|
+
// Wire skip
|
|
8409
|
+
container.querySelector(`#discuss-skip-${data.nodeId}`).addEventListener('click', () => {
|
|
8410
|
+
clearDiscussState();
|
|
8411
|
+
triggerDerivation(data.nodeId);
|
|
8412
|
+
});
|
|
8413
|
+
} catch (_) {}
|
|
8414
|
+
});
|
|
8415
|
+
|
|
8416
|
+
// ─── Agent lifecycle SSE events (M-44) ───────────────────────────────────
|
|
8417
|
+
es.addEventListener('agent-start', function(e) {
|
|
8418
|
+
try {
|
|
8419
|
+
const agent = JSON.parse(e.data);
|
|
8420
|
+
agentCardState.set(agent.id, agent);
|
|
8421
|
+
renderAgentPanel();
|
|
8422
|
+
// Flash pulse indicator
|
|
8423
|
+
if ($activityPulse) {
|
|
8424
|
+
$activityPulse.classList.add('live');
|
|
8425
|
+
clearTimeout($activityPulse._timer);
|
|
8426
|
+
$activityPulse._timer = setTimeout(() => $activityPulse.classList.remove('live'), 3000);
|
|
8427
|
+
}
|
|
8428
|
+
} catch (_) {}
|
|
8429
|
+
});
|
|
8430
|
+
es.addEventListener('agent-update', function(e) {
|
|
8431
|
+
try {
|
|
8432
|
+
const agent = JSON.parse(e.data);
|
|
8433
|
+
// Merge with existing state to preserve any client-side additions
|
|
8434
|
+
const existing = agentCardState.get(agent.id);
|
|
8435
|
+
agentCardState.set(agent.id, Object.assign({}, existing || {}, agent));
|
|
8436
|
+
renderAgentPanel();
|
|
8437
|
+
} catch (_) {}
|
|
8438
|
+
});
|
|
8439
|
+
es.addEventListener('agent-complete', function(e) {
|
|
8440
|
+
try {
|
|
8441
|
+
const agent = JSON.parse(e.data);
|
|
8442
|
+
const existing = agentCardState.get(agent.id);
|
|
8443
|
+
agentCardState.set(agent.id, Object.assign({}, existing || {}, agent));
|
|
8444
|
+
renderAgentPanel();
|
|
8445
|
+
// Flash pulse
|
|
8446
|
+
if ($activityPulse) {
|
|
8447
|
+
$activityPulse.classList.add('live');
|
|
8448
|
+
clearTimeout($activityPulse._timer);
|
|
8449
|
+
$activityPulse._timer = setTimeout(() => $activityPulse.classList.remove('live'), 3000);
|
|
8450
|
+
}
|
|
8451
|
+
} catch (_) {}
|
|
8452
|
+
});
|
|
8453
|
+
|
|
8454
|
+
// Command output — stream text into command output area
|
|
8455
|
+
es.addEventListener('command-output', function(e) {
|
|
8456
|
+
try {
|
|
8457
|
+
const data = JSON.parse(e.data);
|
|
8458
|
+
const outputEl = document.getElementById('command-output-stream');
|
|
8459
|
+
if (outputEl) {
|
|
8460
|
+
if (outputEl.textContent === 'Running...') outputEl.textContent = '';
|
|
8461
|
+
outputEl.textContent += data.text;
|
|
8462
|
+
outputEl.scrollTop = outputEl.scrollHeight;
|
|
8463
|
+
}
|
|
8464
|
+
} catch (_) {}
|
|
8465
|
+
});
|
|
8466
|
+
|
|
8467
|
+
// Command complete — reload graph to reflect changes
|
|
8468
|
+
es.addEventListener('command-complete', function(e) {
|
|
8469
|
+
try {
|
|
8470
|
+
const data = JSON.parse(e.data);
|
|
8471
|
+
// Update command card UI
|
|
8472
|
+
const outputEl = document.getElementById('command-output-stream');
|
|
8473
|
+
if (outputEl) {
|
|
8474
|
+
if (data.error) {
|
|
8475
|
+
outputEl.textContent += '\n\nError: ' + data.error;
|
|
8476
|
+
}
|
|
8477
|
+
outputEl.classList.remove('streaming');
|
|
8478
|
+
}
|
|
8479
|
+
const cmdCard = document.getElementById('command-card');
|
|
8480
|
+
if (cmdCard) { cmdCard.classList.remove('running'); cmdCard.classList.add('editing'); }
|
|
8481
|
+
if (data.exitCode === 0) {
|
|
8482
|
+
loadData().then(() => renderDrillView()); // refresh graph since command may have modified files
|
|
8483
|
+
loadActivity();
|
|
8484
|
+
}
|
|
8485
|
+
} catch (_) {}
|
|
8486
|
+
});
|
|
8487
|
+
|
|
8488
|
+
// Onboarding flow SSE events
|
|
8489
|
+
es.addEventListener('onboard-output', function(e) {
|
|
8490
|
+
try {
|
|
8491
|
+
const data = JSON.parse(e.data);
|
|
8492
|
+
onboardStreamText += data.text;
|
|
8493
|
+
const el = document.getElementById('onboard-stream');
|
|
8494
|
+
if (el) {
|
|
8495
|
+
el.textContent = onboardStreamText;
|
|
8496
|
+
el.scrollTop = el.scrollHeight;
|
|
8497
|
+
}
|
|
8498
|
+
} catch (_) {}
|
|
8499
|
+
});
|
|
8500
|
+
|
|
8501
|
+
es.addEventListener('onboard-questions-complete', function(e) {
|
|
8502
|
+
try {
|
|
8503
|
+
const data = JSON.parse(e.data);
|
|
8504
|
+
if (data.error) {
|
|
8505
|
+
onboardPhase = 'idle';
|
|
8506
|
+
renderDrillView();
|
|
8507
|
+
return;
|
|
8508
|
+
}
|
|
8509
|
+
onboardQuestions = data.questions;
|
|
8510
|
+
onboardStreamText = '';
|
|
8511
|
+
renderOnboardUI();
|
|
8512
|
+
} catch (_) {}
|
|
8513
|
+
});
|
|
8514
|
+
|
|
8515
|
+
es.addEventListener('onboard-proposals-complete', function(e) {
|
|
8516
|
+
try {
|
|
8517
|
+
const data = JSON.parse(e.data);
|
|
8518
|
+
if (data.error) {
|
|
8519
|
+
onboardPhase = 'idle';
|
|
8520
|
+
renderDrillView();
|
|
8521
|
+
return;
|
|
8522
|
+
}
|
|
8523
|
+
onboardProposals = data.proposals;
|
|
8524
|
+
onboardStreamText = '';
|
|
8525
|
+
renderOnboardUI();
|
|
8526
|
+
} catch (_) {}
|
|
8527
|
+
});
|
|
8528
|
+
|
|
6813
8529
|
es.addEventListener('error', () => {
|
|
6814
|
-
// Connection dropped — reconnect after 3s
|
|
6815
8530
|
es.close();
|
|
6816
|
-
|
|
8531
|
+
showReconnectBanner();
|
|
8532
|
+
setTimeout(connectSSE, sseReconnectDelay);
|
|
8533
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, max 30s
|
|
8534
|
+
sseReconnectDelay = Math.min(sseReconnectDelay * 2, 30000);
|
|
6817
8535
|
});
|
|
6818
8536
|
}
|
|
6819
8537
|
|
|
8538
|
+
/** @type {number} Current SSE reconnection delay (exponential backoff) */
|
|
8539
|
+
let sseReconnectDelay = 1000;
|
|
8540
|
+
|
|
8541
|
+
function showReconnectBanner() {
|
|
8542
|
+
let banner = document.getElementById('reconnect-banner');
|
|
8543
|
+
if (!banner) {
|
|
8544
|
+
banner = document.createElement('div');
|
|
8545
|
+
banner.id = 'reconnect-banner';
|
|
8546
|
+
banner.textContent = 'Reconnecting\u2026';
|
|
8547
|
+
document.body.appendChild(banner);
|
|
8548
|
+
}
|
|
8549
|
+
banner.classList.add('visible');
|
|
8550
|
+
}
|
|
8551
|
+
|
|
8552
|
+
function hideReconnectBanner() {
|
|
8553
|
+
const banner = document.getElementById('reconnect-banner');
|
|
8554
|
+
if (banner) banner.classList.remove('visible');
|
|
8555
|
+
}
|
|
8556
|
+
|
|
6820
8557
|
connectSSE();
|
|
6821
8558
|
|
|
8559
|
+
// Prune completed agents older than 30 minutes to prevent unbounded growth
|
|
8560
|
+
setInterval(function() {
|
|
8561
|
+
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
8562
|
+
for (const [id, agent] of agentCardState) {
|
|
8563
|
+
if (agent.status !== 'running' && agent.completedAt) {
|
|
8564
|
+
const completedTs = new Date(agent.completedAt).getTime();
|
|
8565
|
+
if (completedTs < cutoff) agentCardState.delete(id);
|
|
8566
|
+
}
|
|
8567
|
+
}
|
|
8568
|
+
}, 60000);
|
|
8569
|
+
|
|
8570
|
+
// ─── Command card ─────────────────────────────────────────────────────────────
|
|
8571
|
+
|
|
8572
|
+
const $commandCard = document.getElementById('command-card');
|
|
8573
|
+
const $commandInput = document.getElementById('command-card-input');
|
|
8574
|
+
|
|
8575
|
+
if ($commandCard && $commandInput) {
|
|
8576
|
+
// Click to expand
|
|
8577
|
+
$commandCard.addEventListener('click', function() {
|
|
8578
|
+
if (!$commandCard.classList.contains('editing')) {
|
|
8579
|
+
$commandCard.classList.add('editing');
|
|
8580
|
+
$commandInput.value = '';
|
|
8581
|
+
$commandInput.focus();
|
|
8582
|
+
}
|
|
8583
|
+
});
|
|
8584
|
+
|
|
8585
|
+
// Esc to blur — stop propagation to prevent drill-level back-navigation
|
|
8586
|
+
$commandInput.addEventListener('keydown', function(e) {
|
|
8587
|
+
if (e.key === 'Escape') {
|
|
8588
|
+
e.stopPropagation();
|
|
8589
|
+
$commandInput.value = '';
|
|
8590
|
+
$commandInput.blur();
|
|
8591
|
+
}
|
|
8592
|
+
// Enter to send (without shift)
|
|
8593
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
8594
|
+
e.preventDefault();
|
|
8595
|
+
const msg = $commandInput.value.trim();
|
|
8596
|
+
if (!msg) return;
|
|
8597
|
+
$commandInput.value = '';
|
|
8598
|
+
$commandInput.blur();
|
|
8599
|
+
sendCommand(msg);
|
|
8600
|
+
}
|
|
8601
|
+
});
|
|
8602
|
+
|
|
8603
|
+
// Global shortcut: C key (when not in input)
|
|
8604
|
+
document.addEventListener('keydown', function(e) {
|
|
8605
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
|
|
8606
|
+
if (e.key === 'c' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
8607
|
+
// Only if agents tab is visible
|
|
8608
|
+
const agentsTab = document.querySelector('.activity-tab[data-tab="cards"]');
|
|
8609
|
+
if (agentsTab && !agentsTab.classList.contains('active')) {
|
|
8610
|
+
agentsTab.click();
|
|
8611
|
+
}
|
|
8612
|
+
$commandCard.click();
|
|
8613
|
+
e.preventDefault();
|
|
8614
|
+
}
|
|
8615
|
+
});
|
|
8616
|
+
}
|
|
8617
|
+
|
|
8618
|
+
// ─── Onboarding flow functions ─────────────────────────────────────────────────
|
|
8619
|
+
|
|
8620
|
+
function loadOnboardState() {
|
|
8621
|
+
fetch('/api/onboard/state').then(r => r.json()).then(data => {
|
|
8622
|
+
if (!data.active) return;
|
|
8623
|
+
onboardPrompt = data.prompt;
|
|
8624
|
+
onboardQuestions = data.questions;
|
|
8625
|
+
onboardProposals = data.proposals;
|
|
8626
|
+
onboardApproveIndex = data.approveIndex || 0;
|
|
8627
|
+
onboardStreamText = '';
|
|
8628
|
+
|
|
8629
|
+
if (data.phase === 'approving' && data.proposals) {
|
|
8630
|
+
// Mark already-approved proposals
|
|
8631
|
+
for (let i = 0; i < onboardApproveIndex && i < onboardProposals.length; i++) {
|
|
8632
|
+
onboardProposals[i].approvedId = onboardProposals[i].approvedId || '?';
|
|
8633
|
+
}
|
|
8634
|
+
onboardPhase = 'approving';
|
|
8635
|
+
} else if (data.phase === 'proposals' && data.proposals) {
|
|
8636
|
+
onboardPhase = 'proposals';
|
|
8637
|
+
} else if (data.phase === 'questions' && data.questions) {
|
|
8638
|
+
onboardPhase = 'questions';
|
|
8639
|
+
} else {
|
|
8640
|
+
return; // Nothing useful to restore
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
renderOnboardUI();
|
|
8644
|
+
}).catch(() => {});
|
|
8645
|
+
}
|
|
8646
|
+
|
|
8647
|
+
function startOnboard(message) {
|
|
8648
|
+
onboardPhase = 'questions';
|
|
8649
|
+
onboardPrompt = message;
|
|
8650
|
+
onboardQuestions = null;
|
|
8651
|
+
onboardProposals = null;
|
|
8652
|
+
onboardApproveIndex = 0;
|
|
8653
|
+
onboardStreamText = '';
|
|
8654
|
+
renderOnboardUI();
|
|
8655
|
+
|
|
8656
|
+
fetch('/api/onboard', {
|
|
8657
|
+
method: 'POST',
|
|
8658
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8659
|
+
body: JSON.stringify({ message }),
|
|
8660
|
+
}).then(r => r.json()).then(data => {
|
|
8661
|
+
if (data.error) {
|
|
8662
|
+
onboardPhase = 'idle';
|
|
8663
|
+
renderDrillView();
|
|
8664
|
+
}
|
|
8665
|
+
}).catch(() => {
|
|
8666
|
+
onboardPhase = 'idle';
|
|
8667
|
+
renderDrillView();
|
|
8668
|
+
});
|
|
8669
|
+
}
|
|
8670
|
+
|
|
8671
|
+
function submitOnboardAnswers() {
|
|
8672
|
+
const textareas = document.querySelectorAll('.onboard-question textarea');
|
|
8673
|
+
const answers = [];
|
|
8674
|
+
if (onboardQuestions) {
|
|
8675
|
+
onboardQuestions.forEach((q, i) => {
|
|
8676
|
+
const ta = textareas[i];
|
|
8677
|
+
answers.push({ question: q.question, answer: ta ? ta.value.trim() : '' });
|
|
8678
|
+
});
|
|
8679
|
+
}
|
|
8680
|
+
|
|
8681
|
+
onboardPhase = 'proposals';
|
|
8682
|
+
onboardStreamText = '';
|
|
8683
|
+
renderOnboardUI();
|
|
8684
|
+
|
|
8685
|
+
fetch('/api/onboard/answer', {
|
|
8686
|
+
method: 'POST',
|
|
8687
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8688
|
+
body: JSON.stringify({ answers }),
|
|
8689
|
+
}).then(r => r.json()).then(data => {
|
|
8690
|
+
if (data.error) {
|
|
8691
|
+
onboardPhase = 'idle';
|
|
8692
|
+
renderDrillView();
|
|
8693
|
+
}
|
|
8694
|
+
}).catch(() => {
|
|
8695
|
+
onboardPhase = 'idle';
|
|
8696
|
+
renderDrillView();
|
|
8697
|
+
});
|
|
8698
|
+
}
|
|
8699
|
+
|
|
8700
|
+
function skipOnboardQuestions() {
|
|
8701
|
+
onboardPhase = 'proposals';
|
|
8702
|
+
onboardStreamText = '';
|
|
8703
|
+
renderOnboardUI();
|
|
8704
|
+
|
|
8705
|
+
fetch('/api/onboard/answer', {
|
|
8706
|
+
method: 'POST',
|
|
8707
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8708
|
+
body: JSON.stringify({ answers: [] }),
|
|
8709
|
+
}).then(r => r.json()).catch(() => {
|
|
8710
|
+
onboardPhase = 'idle';
|
|
8711
|
+
renderDrillView();
|
|
8712
|
+
});
|
|
8713
|
+
}
|
|
8714
|
+
|
|
8715
|
+
function cancelOnboard() {
|
|
8716
|
+
fetch('/api/onboard/cancel', { method: 'POST' }).catch(() => {});
|
|
8717
|
+
onboardPhase = 'idle';
|
|
8718
|
+
onboardPrompt = null;
|
|
8719
|
+
onboardQuestions = null;
|
|
8720
|
+
onboardProposals = null;
|
|
8721
|
+
renderDrillView();
|
|
8722
|
+
}
|
|
8723
|
+
|
|
8724
|
+
function startOnboardApproving(mode) {
|
|
8725
|
+
onboardPhase = 'approving';
|
|
8726
|
+
onboardApproveIndex = 0;
|
|
8727
|
+
if (mode === 'all') {
|
|
8728
|
+
approveAllOnboard();
|
|
8729
|
+
} else {
|
|
8730
|
+
renderOnboardUI();
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
async function approveCurrentOnboard() {
|
|
8735
|
+
if (!onboardProposals || onboardApproveIndex >= onboardProposals.length) return;
|
|
8736
|
+
|
|
8737
|
+
const proposal = onboardProposals[onboardApproveIndex];
|
|
8738
|
+
// Read potentially edited values from inputs
|
|
8739
|
+
const titleInput = document.querySelector('.onboard-proposal.current .onboard-title');
|
|
8740
|
+
const stmtInput = document.querySelector('.onboard-proposal.current .onboard-statement');
|
|
8741
|
+
const title = titleInput ? titleInput.value.trim() : proposal.title;
|
|
8742
|
+
const statement = stmtInput ? stmtInput.value.trim() : proposal.statement;
|
|
8743
|
+
|
|
8744
|
+
try {
|
|
8745
|
+
const resp = await fetch('/api/onboard/approve', {
|
|
8746
|
+
method: 'POST',
|
|
8747
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8748
|
+
body: JSON.stringify({ title, statement }),
|
|
8749
|
+
});
|
|
8750
|
+
const data = await resp.json();
|
|
8751
|
+
if (data.id) {
|
|
8752
|
+
proposal.approvedId = data.id;
|
|
8753
|
+
}
|
|
8754
|
+
} catch (_) {}
|
|
8755
|
+
|
|
8756
|
+
onboardApproveIndex++;
|
|
8757
|
+
if (onboardApproveIndex >= onboardProposals.length) {
|
|
8758
|
+
// All done — reset and refresh
|
|
8759
|
+
onboardPhase = 'idle';
|
|
8760
|
+
onboardPrompt = null;
|
|
8761
|
+
onboardQuestions = null;
|
|
8762
|
+
onboardProposals = null;
|
|
8763
|
+
fetch('/api/onboard/complete', { method: 'POST' }).catch(() => {});
|
|
8764
|
+
loadData().then(() => renderDrillView());
|
|
8765
|
+
loadActivity();
|
|
8766
|
+
} else {
|
|
8767
|
+
renderOnboardUI();
|
|
8768
|
+
}
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
async function approveAllOnboard() {
|
|
8772
|
+
if (!onboardProposals) return;
|
|
8773
|
+
|
|
8774
|
+
for (let i = 0; i < onboardProposals.length; i++) {
|
|
8775
|
+
onboardApproveIndex = i;
|
|
8776
|
+
renderOnboardUI();
|
|
8777
|
+
const proposal = onboardProposals[i];
|
|
8778
|
+
try {
|
|
8779
|
+
const resp = await fetch('/api/onboard/approve', {
|
|
8780
|
+
method: 'POST',
|
|
8781
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8782
|
+
body: JSON.stringify({ title: proposal.title, statement: proposal.statement }),
|
|
8783
|
+
});
|
|
8784
|
+
const data = await resp.json();
|
|
8785
|
+
if (data.id) proposal.approvedId = data.id;
|
|
8786
|
+
} catch (_) {}
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
onboardPhase = 'idle';
|
|
8790
|
+
onboardPrompt = null;
|
|
8791
|
+
onboardQuestions = null;
|
|
8792
|
+
onboardProposals = null;
|
|
8793
|
+
fetch('/api/onboard/complete', { method: 'POST' }).catch(() => {});
|
|
8794
|
+
loadData().then(() => renderDrillView());
|
|
8795
|
+
loadActivity();
|
|
8796
|
+
}
|
|
8797
|
+
|
|
8798
|
+
function renderOnboardUI() {
|
|
8799
|
+
if (!$drillList) return;
|
|
8800
|
+
$drillList.innerHTML = '';
|
|
8801
|
+
|
|
8802
|
+
const container = document.createElement('div');
|
|
8803
|
+
container.className = 'onboard-container';
|
|
8804
|
+
|
|
8805
|
+
if (onboardPhase === 'questions') {
|
|
8806
|
+
if (!onboardQuestions) {
|
|
8807
|
+
// Still streaming
|
|
8808
|
+
container.innerHTML = `
|
|
8809
|
+
<div class="onboard-phase-label">Analyzing your vision...</div>
|
|
8810
|
+
<pre class="onboard-stream streaming" id="onboard-stream">${escHtml(onboardStreamText) || 'Thinking...'}</pre>
|
|
8811
|
+
`;
|
|
8812
|
+
} else {
|
|
8813
|
+
// Show question form
|
|
8814
|
+
let html = '<div class="onboard-phase-label">Clarification Questions</div>';
|
|
8815
|
+
html += '<div class="onboard-questions">';
|
|
8816
|
+
onboardQuestions.forEach((q, i) => {
|
|
8817
|
+
html += `<div class="onboard-question">
|
|
8818
|
+
<span class="oq-number">Q${i + 1}</span>
|
|
8819
|
+
<label>${escHtml(q.question)}</label>
|
|
8820
|
+
${q.context ? `<div class="oq-context">${escHtml(q.context)}</div>` : ''}
|
|
8821
|
+
${q.options && q.options.length > 0 ? `<div class="oq-options">${q.options.map(o => `<span class="oq-option" data-qi="${i}" data-val="${escHtml(o)}">${escHtml(o)}</span>`).join('')}</div>` : ''}
|
|
8822
|
+
<textarea placeholder="Your answer..." rows="2"></textarea>
|
|
8823
|
+
</div>`;
|
|
8824
|
+
});
|
|
8825
|
+
html += '</div>';
|
|
8826
|
+
html += `<div class="onboard-actions">
|
|
8827
|
+
<button class="onboard-btn-primary" onclick="submitOnboardAnswers()">Submit Answers</button>
|
|
8828
|
+
<button class="onboard-btn-secondary" onclick="skipOnboardQuestions()">Skip</button>
|
|
8829
|
+
<button class="onboard-btn-secondary" onclick="cancelOnboard()">Cancel</button>
|
|
8830
|
+
</div>`;
|
|
8831
|
+
container.innerHTML = html;
|
|
8832
|
+
|
|
8833
|
+
// Attach option click handlers after DOM insertion
|
|
8834
|
+
setTimeout(() => {
|
|
8835
|
+
container.querySelectorAll('.oq-option').forEach(opt => {
|
|
8836
|
+
opt.addEventListener('click', () => {
|
|
8837
|
+
const qi = parseInt(opt.dataset.qi);
|
|
8838
|
+
const val = opt.dataset.val;
|
|
8839
|
+
const ta = container.querySelectorAll('.onboard-question textarea')[qi];
|
|
8840
|
+
if (ta) {
|
|
8841
|
+
ta.value = ta.value ? ta.value + ', ' + val : val;
|
|
8842
|
+
}
|
|
8843
|
+
opt.classList.toggle('selected');
|
|
8844
|
+
});
|
|
8845
|
+
});
|
|
8846
|
+
}, 0);
|
|
8847
|
+
}
|
|
8848
|
+
} else if (onboardPhase === 'proposals') {
|
|
8849
|
+
if (!onboardProposals) {
|
|
8850
|
+
container.innerHTML = `
|
|
8851
|
+
<div class="onboard-phase-label">Generating declarations...</div>
|
|
8852
|
+
<pre class="onboard-stream streaming" id="onboard-stream">${escHtml(onboardStreamText) || 'Thinking...'}</pre>
|
|
8853
|
+
`;
|
|
8854
|
+
} else {
|
|
8855
|
+
let html = '<div class="onboard-phase-label">Proposed Declarations</div>';
|
|
8856
|
+
onboardProposals.forEach((p, i) => {
|
|
8857
|
+
html += `<div class="onboard-proposal">
|
|
8858
|
+
<div class="op-header">
|
|
8859
|
+
<span class="op-index">${i + 1}.</span>
|
|
8860
|
+
</div>
|
|
8861
|
+
<input class="onboard-title" value="${escHtml(p.title)}" />
|
|
8862
|
+
<textarea class="onboard-statement" rows="2">${escHtml(p.statement)}</textarea>
|
|
8863
|
+
<div class="onboard-reason">${escHtml(p.reasoning || '')}</div>
|
|
8864
|
+
</div>`;
|
|
8865
|
+
});
|
|
8866
|
+
html += `<div class="onboard-actions">
|
|
8867
|
+
<button class="onboard-btn-primary" onclick="startOnboardApproving('all')">Approve All</button>
|
|
8868
|
+
<button class="onboard-btn-secondary" onclick="startOnboardApproving('one')">One by One</button>
|
|
8869
|
+
<button class="onboard-btn-secondary" onclick="cancelOnboard()">Cancel</button>
|
|
8870
|
+
</div>`;
|
|
8871
|
+
container.innerHTML = html;
|
|
8872
|
+
}
|
|
8873
|
+
} else if (onboardPhase === 'approving') {
|
|
8874
|
+
if (!onboardProposals) return;
|
|
8875
|
+
let html = `<div class="onboard-phase-label">Approving Declarations</div>`;
|
|
8876
|
+
html += `<div class="onboard-progress">${onboardApproveIndex} of ${onboardProposals.length} approved</div>`;
|
|
8877
|
+
onboardProposals.forEach((p, i) => {
|
|
8878
|
+
const isApproved = i < onboardApproveIndex || p.approvedId;
|
|
8879
|
+
const isCurrent = i === onboardApproveIndex && !p.approvedId;
|
|
8880
|
+
const cls = isApproved ? 'approved' : isCurrent ? 'current' : '';
|
|
8881
|
+
html += `<div class="onboard-proposal ${cls}">
|
|
8882
|
+
<div class="op-header">
|
|
8883
|
+
${isApproved ? '<span class="op-check">\u2713</span>' : `<span class="op-index">${i + 1}.</span>`}
|
|
8884
|
+
${p.approvedId ? `<span class="op-id">${escHtml(p.approvedId)}</span>` : ''}
|
|
8885
|
+
</div>
|
|
8886
|
+
${isCurrent ? `
|
|
8887
|
+
<input class="onboard-title" value="${escHtml(p.title)}" />
|
|
8888
|
+
<textarea class="onboard-statement" rows="2">${escHtml(p.statement)}</textarea>
|
|
8889
|
+
` : `
|
|
8890
|
+
<div style="font-weight:600;font-size:13px">${escHtml(p.title)}</div>
|
|
8891
|
+
<div style="font-size:12px;color:var(--text-dim);margin-top:4px">${escHtml(p.statement)}</div>
|
|
8892
|
+
`}
|
|
8893
|
+
<div class="onboard-reason">${escHtml(p.reasoning || '')}</div>
|
|
8894
|
+
</div>`;
|
|
8895
|
+
});
|
|
8896
|
+
if (onboardApproveIndex < onboardProposals.length) {
|
|
8897
|
+
html += `<div class="onboard-actions">
|
|
8898
|
+
<button class="onboard-btn-primary" onclick="approveCurrentOnboard()">Approve & Next</button>
|
|
8899
|
+
<button class="onboard-btn-secondary" onclick="cancelOnboard()">Cancel</button>
|
|
8900
|
+
</div>`;
|
|
8901
|
+
}
|
|
8902
|
+
container.innerHTML = html;
|
|
8903
|
+
}
|
|
8904
|
+
|
|
8905
|
+
$drillList.appendChild(container);
|
|
8906
|
+
}
|
|
8907
|
+
|
|
8908
|
+
function sendCommand(message) {
|
|
8909
|
+
// Route to onboarding when at declarations level with few declarations or long message
|
|
8910
|
+
if (drillLevel === 'declarations' && graphData) {
|
|
8911
|
+
const declCount = (graphData.declarations || []).length;
|
|
8912
|
+
if (declCount < 3 || message.length > 150) {
|
|
8913
|
+
startOnboard(message);
|
|
8914
|
+
return;
|
|
8915
|
+
}
|
|
8916
|
+
}
|
|
8917
|
+
|
|
8918
|
+
// Gather context from current view
|
|
8919
|
+
const context = {};
|
|
8920
|
+
if (drillLevel === 'declarations') {
|
|
8921
|
+
context.viewDescription = 'Declaration list (lifecycle view)';
|
|
8922
|
+
const cards = document.querySelectorAll('#drill-list .drill-card');
|
|
8923
|
+
context.nodeIds = Array.from(cards).map(c => c.dataset.nodeId).filter(Boolean);
|
|
8924
|
+
} else if (drillLevel === 'milestones' && drillDeclId) {
|
|
8925
|
+
context.nodeId = drillDeclId;
|
|
8926
|
+
context.viewDescription = 'Milestones for ' + drillDeclId;
|
|
8927
|
+
const cards = document.querySelectorAll('#drill-list .drill-card');
|
|
8928
|
+
context.nodeIds = Array.from(cards).map(c => c.dataset.nodeId).filter(Boolean);
|
|
8929
|
+
} else if (drillLevel === 'actions' && drillMileId) {
|
|
8930
|
+
context.nodeId = drillMileId;
|
|
8931
|
+
context.viewDescription = 'Actions for ' + drillMileId;
|
|
8932
|
+
const cards = document.querySelectorAll('#drill-list .drill-card');
|
|
8933
|
+
context.nodeIds = Array.from(cards).map(c => c.dataset.nodeId).filter(Boolean);
|
|
8934
|
+
}
|
|
8935
|
+
|
|
8936
|
+
// Show streaming output area in command card
|
|
8937
|
+
const cmdCard = document.getElementById('command-card');
|
|
8938
|
+
if (cmdCard) {
|
|
8939
|
+
cmdCard.classList.add('running');
|
|
8940
|
+
cmdCard.classList.remove('editing');
|
|
8941
|
+
// Create or reuse output area
|
|
8942
|
+
let outputEl = document.getElementById('command-output-stream');
|
|
8943
|
+
if (!outputEl) {
|
|
8944
|
+
outputEl = document.createElement('pre');
|
|
8945
|
+
outputEl.id = 'command-output-stream';
|
|
8946
|
+
outputEl.className = 'command-output-stream streaming';
|
|
8947
|
+
cmdCard.appendChild(outputEl);
|
|
8948
|
+
} else {
|
|
8949
|
+
outputEl.className = 'command-output-stream streaming';
|
|
8950
|
+
}
|
|
8951
|
+
outputEl.textContent = 'Running...';
|
|
8952
|
+
}
|
|
8953
|
+
|
|
8954
|
+
fetch('/api/command', {
|
|
8955
|
+
method: 'POST',
|
|
8956
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8957
|
+
body: JSON.stringify({ message, context }),
|
|
8958
|
+
}).then(r => r.json()).then(data => {
|
|
8959
|
+
if (data.error) {
|
|
8960
|
+
console.error('Command error:', data.error);
|
|
8961
|
+
const outputEl = document.getElementById('command-output-stream');
|
|
8962
|
+
if (outputEl) outputEl.textContent = 'Error: ' + data.error;
|
|
8963
|
+
}
|
|
8964
|
+
}).catch(err => {
|
|
8965
|
+
console.error('Command failed:', err);
|
|
8966
|
+
const outputEl = document.getElementById('command-output-stream');
|
|
8967
|
+
if (outputEl) outputEl.textContent = 'Error: ' + err.message;
|
|
8968
|
+
});
|
|
8969
|
+
}
|
|
8970
|
+
|
|
8971
|
+
// ─── Clickable activity log items ─────────────────────────────────────────────
|
|
8972
|
+
|
|
8973
|
+
if ($activityList) {
|
|
8974
|
+
$activityList.addEventListener('click', function(e) {
|
|
8975
|
+
const item = e.target.closest('.activity-event');
|
|
8976
|
+
if (!item) return;
|
|
8977
|
+
const desc = item.querySelector('.ae-desc');
|
|
8978
|
+
if (!desc) return;
|
|
8979
|
+
// Extract node ID from description like "Review of A-117 complete"
|
|
8980
|
+
const match = desc.textContent.match(/\b([DMA]-\d+)\b/);
|
|
8981
|
+
if (match) {
|
|
8982
|
+
const nodeId = match[1];
|
|
8983
|
+
// Navigate to that node
|
|
8984
|
+
const prefix = nodeId.split('-')[0];
|
|
8985
|
+
if (prefix === 'D') {
|
|
8986
|
+
renderDeclarations(graphData); // back to declarations
|
|
8987
|
+
} else if (prefix === 'M') {
|
|
8988
|
+
// Find parent declaration
|
|
8989
|
+
const milestone = (graphData.milestones || []).find(m => m.id === nodeId);
|
|
8990
|
+
if (milestone && milestone.realizes) {
|
|
8991
|
+
const declId = Array.isArray(milestone.realizes) ? milestone.realizes[0] : milestone.realizes;
|
|
8992
|
+
renderMilestones(declId, graphData);
|
|
8993
|
+
}
|
|
8994
|
+
} else if (prefix === 'A') {
|
|
8995
|
+
// Find parent milestone
|
|
8996
|
+
const action = (graphData.actions || []).find(a => a.id === nodeId);
|
|
8997
|
+
if (action && action.causes && action.causes.length > 0) {
|
|
8998
|
+
renderActions(action.causes[0], graphData);
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
});
|
|
9003
|
+
}
|
|
9004
|
+
|
|
6822
9005
|
// ─── Bootstrap ───────────────────────────────────────────────────────────────
|
|
6823
9006
|
|
|
6824
9007
|
showLoading();
|
|
6825
|
-
loadData().then(() =>
|
|
9008
|
+
loadData().then(() => {
|
|
9009
|
+
restoreExecState();
|
|
9010
|
+
// Restore onboard session from server
|
|
9011
|
+
loadOnboardState();
|
|
9012
|
+
});
|
|
6826
9013
|
loadActivity();
|
|
9014
|
+
loadAgentCards();
|
|
9015
|
+
|
|
9016
|
+
// Poll agent cards every 3s as fallback if SSE agent events are missed
|
|
9017
|
+
setInterval(loadAgentCards, 3000);
|