opencode-dashboard 0.1.0 → 0.2.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 +20 -1
- package/bin/cli.ts +16 -2
- package/dist/assets/{index-W-qyIr7d.js → index-DJanV0JQ.js} +2 -2
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugin/index.ts +245 -5
- package/server/index.ts +23 -3
- package/server/pid.ts +115 -5
- package/server/routes.ts +131 -0
- package/server/sse.ts +16 -0
- package/server/state.ts +518 -1
- package/shared/types.ts +17 -7
- package/shared/version.ts +46 -0
package/server/state.ts
CHANGED
|
@@ -56,6 +56,198 @@ interface SerializedDashboardState {
|
|
|
56
56
|
projects: [string, SerializedProjectState][];
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// --- Constants for dynamic column creation & visibility ---
|
|
60
|
+
|
|
61
|
+
/** Stages that are bookends and should never get dynamic columns */
|
|
62
|
+
const BOOKEND_STAGES = new Set(["ready", "done", "error"]);
|
|
63
|
+
|
|
64
|
+
/** Bookend column IDs that are always visible */
|
|
65
|
+
const ALWAYS_VISIBLE_COLUMNS = BOOKEND_STAGES;
|
|
66
|
+
|
|
67
|
+
/** Pipeline stage IDs — all pipeline agent columns including orchestrator */
|
|
68
|
+
const PIPELINE_STAGE_IDS = new Set([
|
|
69
|
+
"orchestrator",
|
|
70
|
+
"pipeline-builder",
|
|
71
|
+
"pipeline-refactor",
|
|
72
|
+
"pipeline-reviewer",
|
|
73
|
+
"pipeline-committer",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** Grace period in milliseconds before hiding pipeline columns */
|
|
77
|
+
const PIPELINE_HIDE_GRACE_MS = 30_000;
|
|
78
|
+
|
|
79
|
+
/** Pipeline agent IDs with their fixed order */
|
|
80
|
+
const PIPELINE_AGENT_ORDER: Record<string, number> = {
|
|
81
|
+
orchestrator: 1,
|
|
82
|
+
"pipeline-builder": 2,
|
|
83
|
+
"pipeline-refactor": 3,
|
|
84
|
+
"pipeline-reviewer": 4,
|
|
85
|
+
"pipeline-committer": 5,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Default color palette for dynamically created agent columns */
|
|
89
|
+
const DEFAULT_COLUMN_COLORS = [
|
|
90
|
+
"#8b5cf6", // violet
|
|
91
|
+
"#3b82f6", // blue
|
|
92
|
+
"#06b6d4", // cyan
|
|
93
|
+
"#f59e0b", // amber
|
|
94
|
+
"#10b981", // emerald
|
|
95
|
+
"#ec4899", // pink
|
|
96
|
+
"#f97316", // orange
|
|
97
|
+
"#6366f1", // indigo
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Format agent name into a human-readable label.
|
|
102
|
+
* Strips 'pipeline-' prefix and capitalizes the first letter.
|
|
103
|
+
*
|
|
104
|
+
* Examples:
|
|
105
|
+
* 'pipeline-builder' → 'Builder'
|
|
106
|
+
* 'build' → 'Build'
|
|
107
|
+
* 'my-custom-agent' → 'My-custom-agent'
|
|
108
|
+
*/
|
|
109
|
+
export function formatAgentLabel(name: string): string {
|
|
110
|
+
const display = name.startsWith("pipeline-")
|
|
111
|
+
? name.slice("pipeline-".length)
|
|
112
|
+
: name;
|
|
113
|
+
return display.charAt(0).toUpperCase() + display.slice(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a column with the given ID already exists in a project's columns.
|
|
118
|
+
*/
|
|
119
|
+
export function hasColumn(project: ProjectState, stageId: string): boolean {
|
|
120
|
+
return project.columns.some((col) => col.id === stageId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Pick a color from the default palette, avoiding colors already used
|
|
125
|
+
* by existing columns.
|
|
126
|
+
*/
|
|
127
|
+
export function pickColor(project: ProjectState, _stageId: string): string {
|
|
128
|
+
const usedColors = new Set(project.columns.map((col) => col.color));
|
|
129
|
+
for (const color of DEFAULT_COLUMN_COLORS) {
|
|
130
|
+
if (!usedColors.has(color)) {
|
|
131
|
+
return color;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// All colors used — cycle through palette based on column count
|
|
135
|
+
return DEFAULT_COLUMN_COLORS[
|
|
136
|
+
project.columns.length % DEFAULT_COLUMN_COLORS.length
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compute the correct order for a new column, then re-normalize all
|
|
142
|
+
* order values to clean sequential integers.
|
|
143
|
+
*
|
|
144
|
+
* Pipeline agents get their fixed order (1-5).
|
|
145
|
+
* Standalone agents insert before "done".
|
|
146
|
+
* After insertion, all columns are re-ordered to sequential integers.
|
|
147
|
+
*/
|
|
148
|
+
export function computeOrder(project: ProjectState, stageId: string): number {
|
|
149
|
+
// If it's a pipeline agent, assign its fixed order
|
|
150
|
+
if (stageId in PIPELINE_AGENT_ORDER) {
|
|
151
|
+
return PIPELINE_AGENT_ORDER[stageId];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Standalone: insert before "done"
|
|
155
|
+
const doneCol = project.columns.find((c) => c.id === "done");
|
|
156
|
+
if (doneCol) {
|
|
157
|
+
return doneCol.order;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// No "done" column yet — put it at the end
|
|
161
|
+
return project.columns.length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Re-normalize all column order values to clean sequential integers
|
|
166
|
+
* based on the defined ordering rules:
|
|
167
|
+
* 0: ready
|
|
168
|
+
* 1-5: pipeline agents (orchestrator, builder, refactor, reviewer, committer)
|
|
169
|
+
* 6+: standalone agents (alphabetical)
|
|
170
|
+
* second-to-last: done
|
|
171
|
+
* last: error
|
|
172
|
+
*/
|
|
173
|
+
function renormalizeColumnOrders(project: ProjectState): void {
|
|
174
|
+
const ready: ColumnConfig[] = [];
|
|
175
|
+
const pipeline: ColumnConfig[] = [];
|
|
176
|
+
const standalone: ColumnConfig[] = [];
|
|
177
|
+
const done: ColumnConfig[] = [];
|
|
178
|
+
const error: ColumnConfig[] = [];
|
|
179
|
+
|
|
180
|
+
for (const col of project.columns) {
|
|
181
|
+
if (col.id === "ready") {
|
|
182
|
+
ready.push(col);
|
|
183
|
+
} else if (col.id === "done") {
|
|
184
|
+
done.push(col);
|
|
185
|
+
} else if (col.id === "error") {
|
|
186
|
+
error.push(col);
|
|
187
|
+
} else if (col.id in PIPELINE_AGENT_ORDER) {
|
|
188
|
+
pipeline.push(col);
|
|
189
|
+
} else {
|
|
190
|
+
standalone.push(col);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Sort pipeline agents by their fixed order
|
|
195
|
+
pipeline.sort(
|
|
196
|
+
(a, b) =>
|
|
197
|
+
(PIPELINE_AGENT_ORDER[a.id] ?? 99) -
|
|
198
|
+
(PIPELINE_AGENT_ORDER[b.id] ?? 99)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Sort standalone agents alphabetically
|
|
202
|
+
standalone.sort((a, b) => a.id.localeCompare(b.id));
|
|
203
|
+
|
|
204
|
+
// Reassemble and assign sequential order values
|
|
205
|
+
const sorted = [...ready, ...pipeline, ...standalone, ...done, ...error];
|
|
206
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
207
|
+
sorted[i].order = i;
|
|
208
|
+
}
|
|
209
|
+
project.columns = sorted;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Create a new dynamic ColumnConfig for an unknown stage.
|
|
214
|
+
*
|
|
215
|
+
* Returns the new column, or null if:
|
|
216
|
+
* - The stage is a bookend ("ready", "done", "error")
|
|
217
|
+
* - A column with this ID already exists
|
|
218
|
+
*/
|
|
219
|
+
export function createDynamicColumn(
|
|
220
|
+
project: ProjectState,
|
|
221
|
+
stageId: string
|
|
222
|
+
): ColumnConfig | null {
|
|
223
|
+
// Don't create columns for bookend stages
|
|
224
|
+
if (BOOKEND_STAGES.has(stageId)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Don't create duplicates
|
|
229
|
+
if (hasColumn(project, stageId)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const isPipelineAgent = stageId in PIPELINE_AGENT_ORDER;
|
|
234
|
+
|
|
235
|
+
const column: ColumnConfig = {
|
|
236
|
+
id: stageId,
|
|
237
|
+
label: formatAgentLabel(stageId),
|
|
238
|
+
type: "agent",
|
|
239
|
+
color: pickColor(project, stageId),
|
|
240
|
+
order: computeOrder(project, stageId),
|
|
241
|
+
group: isPipelineAgent ? "pipeline" : "standalone",
|
|
242
|
+
source: "dynamic",
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
project.columns.push(column);
|
|
246
|
+
renormalizeColumnOrders(project);
|
|
247
|
+
|
|
248
|
+
return column;
|
|
249
|
+
}
|
|
250
|
+
|
|
59
251
|
// --- StateManager ---
|
|
60
252
|
|
|
61
253
|
export class StateManager {
|
|
@@ -64,6 +256,30 @@ export class StateManager {
|
|
|
64
256
|
private persistDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
65
257
|
private persistDebounceMs: number;
|
|
66
258
|
|
|
259
|
+
/**
|
|
260
|
+
* In-memory active agent tracking per project.
|
|
261
|
+
* Keyed by projectPath. The Set contains agent names that are currently active.
|
|
262
|
+
* Serialized as string[] on ProjectState.activeAgents for JSON output.
|
|
263
|
+
*/
|
|
264
|
+
private activeAgents: Map<string, Set<string>> = new Map();
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* In-memory grace-period timers for pipeline column hiding.
|
|
268
|
+
* When pipeline columns transition from visible → should-hide, a 30s timer
|
|
269
|
+
* is started. If pipeline activity resumes, the timer is cancelled.
|
|
270
|
+
* Keyed by projectPath.
|
|
271
|
+
*/
|
|
272
|
+
private pipelineHideTimers: Map<
|
|
273
|
+
string,
|
|
274
|
+
ReturnType<typeof setTimeout>
|
|
275
|
+
> = new Map();
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Tracks the last visible column key per project to avoid unnecessary broadcasts.
|
|
279
|
+
* Keyed by projectPath. Value is a joined string of visible column IDs.
|
|
280
|
+
*/
|
|
281
|
+
private _lastVisibleColumnsKey: Map<string, string> = new Map();
|
|
282
|
+
|
|
67
283
|
/** Listeners called after every state mutation (for SSE broadcasting) */
|
|
68
284
|
private listeners: Array<(event: string, data: unknown) => void> = [];
|
|
69
285
|
|
|
@@ -120,6 +336,10 @@ export class StateManager {
|
|
|
120
336
|
pipelines,
|
|
121
337
|
lastBeadSnapshot: project.lastBeadSnapshot,
|
|
122
338
|
columns: project.columns,
|
|
339
|
+
visibleColumns: this.computeVisibleColumns(project),
|
|
340
|
+
activeAgents: Array.from(
|
|
341
|
+
this.activeAgents.get(project.projectPath) || []
|
|
342
|
+
),
|
|
123
343
|
});
|
|
124
344
|
}
|
|
125
345
|
return { projects };
|
|
@@ -153,6 +373,10 @@ export class StateManager {
|
|
|
153
373
|
};
|
|
154
374
|
this.state.projects.set(projectPath, project);
|
|
155
375
|
}
|
|
376
|
+
// Ensure activeAgents Set exists for this project
|
|
377
|
+
if (!this.activeAgents.has(projectPath)) {
|
|
378
|
+
this.activeAgents.set(projectPath, new Set());
|
|
379
|
+
}
|
|
156
380
|
this.schedulePersist();
|
|
157
381
|
}
|
|
158
382
|
|
|
@@ -161,6 +385,11 @@ export class StateManager {
|
|
|
161
385
|
for (const [, project] of this.state.projects) {
|
|
162
386
|
if (project.pluginId === pluginId) {
|
|
163
387
|
project.connected = false;
|
|
388
|
+
// Clear active agents for this project
|
|
389
|
+
const agents = this.activeAgents.get(project.projectPath);
|
|
390
|
+
if (agents) {
|
|
391
|
+
agents.clear();
|
|
392
|
+
}
|
|
164
393
|
// No active session — mark active pipelines as idle
|
|
165
394
|
for (const [, pipeline] of project.pipelines) {
|
|
166
395
|
if (pipeline.status === "active") {
|
|
@@ -195,6 +424,199 @@ export class StateManager {
|
|
|
195
424
|
return null;
|
|
196
425
|
}
|
|
197
426
|
|
|
427
|
+
/** Get the active agents Set for a project (by projectPath) */
|
|
428
|
+
getActiveAgents(projectPath: string): Set<string> {
|
|
429
|
+
let agents = this.activeAgents.get(projectPath);
|
|
430
|
+
if (!agents) {
|
|
431
|
+
agents = new Set();
|
|
432
|
+
this.activeAgents.set(projectPath, agents);
|
|
433
|
+
}
|
|
434
|
+
return agents;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// --- Column Visibility ---
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if any bead across all pipelines has a stage matching any of
|
|
441
|
+
* the given stage IDs.
|
|
442
|
+
*/
|
|
443
|
+
anyBeadInStages(project: ProjectState, stageIds: Set<string>): boolean {
|
|
444
|
+
for (const [, pipeline] of project.pipelines) {
|
|
445
|
+
for (const [, bead] of pipeline.beads) {
|
|
446
|
+
if (stageIds.has(bead.stage)) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Determine whether the pipeline group should be visible, considering
|
|
456
|
+
* active agents, bead stages, and grace period timers.
|
|
457
|
+
*
|
|
458
|
+
* This is a pure read — it does NOT start or cancel timers.
|
|
459
|
+
* Timer management is done in broadcastColumnsUpdate.
|
|
460
|
+
*/
|
|
461
|
+
private isPipelineVisible(project: ProjectState): boolean {
|
|
462
|
+
const activeAgents = this.getActiveAgents(project.projectPath);
|
|
463
|
+
const orchestratorActive = activeAgents.has("orchestrator");
|
|
464
|
+
const anyBeadInPipeline = this.anyBeadInStages(project, PIPELINE_STAGE_IDS);
|
|
465
|
+
|
|
466
|
+
if (orchestratorActive || anyBeadInPipeline) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Check if any pipeline columns actually exist in the project
|
|
471
|
+
const hasPipelineColumns = project.columns.some(
|
|
472
|
+
(col) => col.group === "pipeline"
|
|
473
|
+
);
|
|
474
|
+
if (!hasPipelineColumns) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Pipeline should hide — but is a grace period timer running?
|
|
479
|
+
return this.pipelineHideTimers.has(project.projectPath);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Compute the set of visible columns for a project based on:
|
|
484
|
+
* 1. Bookend columns (ready, done, error) are always visible
|
|
485
|
+
* 2. Pipeline group: visible when orchestrator is active OR any bead
|
|
486
|
+
* occupies a pipeline stage OR grace period timer is running.
|
|
487
|
+
* Show/hide as a unit.
|
|
488
|
+
* 3. Standalone columns: visible when their agent is active OR any bead
|
|
489
|
+
* occupies that column's stage.
|
|
490
|
+
*
|
|
491
|
+
* This is a pure read with no side effects (does not start timers).
|
|
492
|
+
* Returns columns sorted by order.
|
|
493
|
+
*/
|
|
494
|
+
computeVisibleColumns(project: ProjectState): ColumnConfig[] {
|
|
495
|
+
const activeAgents = this.getActiveAgents(project.projectPath);
|
|
496
|
+
const pipelineVisible = this.isPipelineVisible(project);
|
|
497
|
+
|
|
498
|
+
const visible: ColumnConfig[] = [];
|
|
499
|
+
|
|
500
|
+
for (const col of project.columns) {
|
|
501
|
+
// 1. Always include bookend columns
|
|
502
|
+
if (ALWAYS_VISIBLE_COLUMNS.has(col.id)) {
|
|
503
|
+
visible.push(col);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 2. Pipeline group columns — show/hide as a unit
|
|
508
|
+
if (col.group === "pipeline") {
|
|
509
|
+
if (pipelineVisible) {
|
|
510
|
+
visible.push(col);
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 3. Standalone columns — show if agent active OR any bead in that stage
|
|
516
|
+
const agentActive = activeAgents.has(col.id);
|
|
517
|
+
const beadInStage = this.anyBeadInStages(
|
|
518
|
+
project,
|
|
519
|
+
new Set([col.id])
|
|
520
|
+
);
|
|
521
|
+
if (agentActive || beadInStage) {
|
|
522
|
+
visible.push(col);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Sort by order
|
|
527
|
+
visible.sort((a, b) => a.order - b.order);
|
|
528
|
+
return visible;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Manage grace period timers for pipeline column hiding.
|
|
533
|
+
*
|
|
534
|
+
* Called by broadcastColumnsUpdate when pipeline state changes.
|
|
535
|
+
* - If pipeline is active: cancel any pending hide timer
|
|
536
|
+
* - If pipeline should hide and was previously visible: start grace timer
|
|
537
|
+
* - If pipeline was never visible: no timer needed
|
|
538
|
+
*/
|
|
539
|
+
private managePipelineGracePeriod(project: ProjectState): void {
|
|
540
|
+
const activeAgents = this.getActiveAgents(project.projectPath);
|
|
541
|
+
const projectPath = project.projectPath;
|
|
542
|
+
const orchestratorActive = activeAgents.has("orchestrator");
|
|
543
|
+
const anyBeadInPipeline = this.anyBeadInStages(project, PIPELINE_STAGE_IDS);
|
|
544
|
+
const pipelineShouldBeActive = orchestratorActive || anyBeadInPipeline;
|
|
545
|
+
|
|
546
|
+
const existingTimer = this.pipelineHideTimers.get(projectPath);
|
|
547
|
+
|
|
548
|
+
if (pipelineShouldBeActive) {
|
|
549
|
+
// Pipeline is active — cancel any pending hide timer
|
|
550
|
+
if (existingTimer) {
|
|
551
|
+
clearTimeout(existingTimer);
|
|
552
|
+
this.pipelineHideTimers.delete(projectPath);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Pipeline should hide — do we need a grace period?
|
|
558
|
+
if (existingTimer) {
|
|
559
|
+
// Timer already running — let it continue
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check if pipeline was previously visible (had a key with pipeline cols)
|
|
564
|
+
const lastKey = this._lastVisibleColumnsKey.get(projectPath) || "";
|
|
565
|
+
const wasPipelineVisible = [...PIPELINE_STAGE_IDS].some((id) =>
|
|
566
|
+
lastKey.split(",").includes(id)
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
if (wasPipelineVisible) {
|
|
570
|
+
// Transition from visible → should-hide: start grace period
|
|
571
|
+
const timer = setTimeout(() => {
|
|
572
|
+
this.pipelineHideTimers.delete(projectPath);
|
|
573
|
+
// Clear the last key so managePipelineGracePeriod doesn't
|
|
574
|
+
// think pipeline was previously visible and start another timer.
|
|
575
|
+
this._lastVisibleColumnsKey.delete(projectPath);
|
|
576
|
+
// Grace period elapsed — recompute and broadcast
|
|
577
|
+
this.broadcastColumnsUpdate(project);
|
|
578
|
+
}, PIPELINE_HIDE_GRACE_MS);
|
|
579
|
+
// Don't block process exit
|
|
580
|
+
if (timer && typeof timer === "object" && "unref" in timer) {
|
|
581
|
+
timer.unref();
|
|
582
|
+
}
|
|
583
|
+
this.pipelineHideTimers.set(projectPath, timer);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Recompute visible columns for a project and track whether they changed.
|
|
589
|
+
* If the visible set changed, updates the tracking key and notifies listeners.
|
|
590
|
+
* Also manages grace period timers for pipeline column hiding.
|
|
591
|
+
*
|
|
592
|
+
* Returns true if the visible columns changed, false otherwise.
|
|
593
|
+
*/
|
|
594
|
+
broadcastColumnsUpdate(project: ProjectState): boolean {
|
|
595
|
+
// Manage grace period timers BEFORE computing visible columns,
|
|
596
|
+
// so that computeVisibleColumns sees the correct timer state.
|
|
597
|
+
this.managePipelineGracePeriod(project);
|
|
598
|
+
|
|
599
|
+
const visible = this.computeVisibleColumns(project);
|
|
600
|
+
const newKey = visible.map((c) => c.id).join(",");
|
|
601
|
+
const oldKey = this._lastVisibleColumnsKey.get(project.projectPath) || "";
|
|
602
|
+
|
|
603
|
+
if (newKey === oldKey) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
this._lastVisibleColumnsKey.set(project.projectPath, newKey);
|
|
608
|
+
|
|
609
|
+
// Notify listeners about column visibility change
|
|
610
|
+
for (const listener of this.listeners) {
|
|
611
|
+
listener("columns:visibility", {
|
|
612
|
+
projectPath: project.projectPath,
|
|
613
|
+
visibleColumns: visible,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
|
|
198
620
|
// --- Event Processing ---
|
|
199
621
|
|
|
200
622
|
/**
|
|
@@ -344,6 +766,9 @@ export class StateManager {
|
|
|
344
766
|
}
|
|
345
767
|
pipeline.currentBeadId = beadId;
|
|
346
768
|
pipeline.status = "active";
|
|
769
|
+
|
|
770
|
+
// Auto-create column for unknown stages
|
|
771
|
+
createDynamicColumn(project, stage);
|
|
347
772
|
}
|
|
348
773
|
|
|
349
774
|
if (beadRecord) {
|
|
@@ -351,6 +776,9 @@ export class StateManager {
|
|
|
351
776
|
}
|
|
352
777
|
this.schedulePersist();
|
|
353
778
|
|
|
779
|
+
// Recompute column visibility (bead claimed → stage changed)
|
|
780
|
+
this.broadcastColumnsUpdate(project);
|
|
781
|
+
|
|
354
782
|
return {
|
|
355
783
|
event: "bead:claimed",
|
|
356
784
|
data: {
|
|
@@ -380,9 +808,15 @@ export class StateManager {
|
|
|
380
808
|
beadState.agentSessionId = agentSessionId;
|
|
381
809
|
}
|
|
382
810
|
}
|
|
811
|
+
|
|
812
|
+
// Auto-create column for unknown stages
|
|
813
|
+
createDynamicColumn(project, stage);
|
|
383
814
|
}
|
|
384
815
|
this.schedulePersist();
|
|
385
816
|
|
|
817
|
+
// Recompute column visibility (bead stage changed)
|
|
818
|
+
this.broadcastColumnsUpdate(project);
|
|
819
|
+
|
|
386
820
|
return {
|
|
387
821
|
event: "bead:stage",
|
|
388
822
|
data: {
|
|
@@ -422,6 +856,9 @@ export class StateManager {
|
|
|
422
856
|
}
|
|
423
857
|
this.schedulePersist();
|
|
424
858
|
|
|
859
|
+
// Recompute column visibility (bead done → may affect pipeline visibility)
|
|
860
|
+
this.broadcastColumnsUpdate(project);
|
|
861
|
+
|
|
425
862
|
return {
|
|
426
863
|
event: "bead:done",
|
|
427
864
|
data: {
|
|
@@ -476,6 +913,9 @@ export class StateManager {
|
|
|
476
913
|
}
|
|
477
914
|
this.schedulePersist();
|
|
478
915
|
|
|
916
|
+
// Recompute column visibility (bead error → may affect column visibility)
|
|
917
|
+
this.broadcastColumnsUpdate(project);
|
|
918
|
+
|
|
479
919
|
return {
|
|
480
920
|
event: "bead:error",
|
|
481
921
|
data: {
|
|
@@ -588,6 +1028,9 @@ export class StateManager {
|
|
|
588
1028
|
}
|
|
589
1029
|
this.schedulePersist();
|
|
590
1030
|
|
|
1031
|
+
// Recompute column visibility (bead removed → may affect column visibility)
|
|
1032
|
+
this.broadcastColumnsUpdate(project);
|
|
1033
|
+
|
|
591
1034
|
return {
|
|
592
1035
|
event: "bead:removed",
|
|
593
1036
|
data: {
|
|
@@ -604,6 +1047,7 @@ export class StateManager {
|
|
|
604
1047
|
): { event: string; data: Record<string, unknown> } {
|
|
605
1048
|
const beadId = data.beadId as string;
|
|
606
1049
|
const sessionId = data.sessionId as string;
|
|
1050
|
+
const agentName = data.agent as string;
|
|
607
1051
|
const pipeline = this.getOrCreateDefaultPipeline(project);
|
|
608
1052
|
|
|
609
1053
|
if (beadId && sessionId) {
|
|
@@ -613,6 +1057,26 @@ export class StateManager {
|
|
|
613
1057
|
}
|
|
614
1058
|
}
|
|
615
1059
|
|
|
1060
|
+
// Track active agent
|
|
1061
|
+
if (agentName) {
|
|
1062
|
+
let agents = this.activeAgents.get(project.projectPath);
|
|
1063
|
+
if (!agents) {
|
|
1064
|
+
agents = new Set();
|
|
1065
|
+
this.activeAgents.set(project.projectPath, agents);
|
|
1066
|
+
}
|
|
1067
|
+
agents.add(agentName);
|
|
1068
|
+
|
|
1069
|
+
// Create dynamic column if one doesn't exist for this agent.
|
|
1070
|
+
// Built-in agents (Build, Plan, Explore, etc.) don't have .md config
|
|
1071
|
+
// files, so they won't have columns from the initial column config.
|
|
1072
|
+
if (!hasColumn(project, agentName)) {
|
|
1073
|
+
createDynamicColumn(project, agentName);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Recompute column visibility (agent became active → may show columns)
|
|
1078
|
+
this.broadcastColumnsUpdate(project);
|
|
1079
|
+
|
|
616
1080
|
return {
|
|
617
1081
|
event: "agent:active",
|
|
618
1082
|
data: {
|
|
@@ -629,6 +1093,7 @@ export class StateManager {
|
|
|
629
1093
|
): { event: string; data: Record<string, unknown> } {
|
|
630
1094
|
const beadId = data.beadId as string;
|
|
631
1095
|
const sessionId = data.sessionId as string;
|
|
1096
|
+
const agentName = data.agent as string;
|
|
632
1097
|
const pipeline = this.getOrCreateDefaultPipeline(project);
|
|
633
1098
|
|
|
634
1099
|
if (beadId) {
|
|
@@ -638,6 +1103,17 @@ export class StateManager {
|
|
|
638
1103
|
}
|
|
639
1104
|
}
|
|
640
1105
|
|
|
1106
|
+
// Remove agent from active set
|
|
1107
|
+
if (agentName) {
|
|
1108
|
+
const agents = this.activeAgents.get(project.projectPath);
|
|
1109
|
+
if (agents) {
|
|
1110
|
+
agents.delete(agentName);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Recompute column visibility (agent went idle → may hide columns)
|
|
1115
|
+
this.broadcastColumnsUpdate(project);
|
|
1116
|
+
|
|
641
1117
|
return {
|
|
642
1118
|
event: "agent:idle",
|
|
643
1119
|
data: {
|
|
@@ -713,16 +1189,42 @@ export class StateManager {
|
|
|
713
1189
|
const columns = data.columns as ColumnConfig[] | undefined;
|
|
714
1190
|
|
|
715
1191
|
if (Array.isArray(columns)) {
|
|
716
|
-
|
|
1192
|
+
// Merge strategy: keep dynamic columns that aren't in the new set,
|
|
1193
|
+
// replace/update discovered columns with the new ones from the plugin.
|
|
1194
|
+
// This prevents losing dynamic columns when a plugin reconnects.
|
|
1195
|
+
const existingDynamic = project.columns.filter(
|
|
1196
|
+
(col) => col.source === "dynamic"
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
// Build a set of IDs from the incoming plugin columns
|
|
1200
|
+
const incomingIds = new Set(columns.map((c) => c.id));
|
|
1201
|
+
|
|
1202
|
+
// Keep dynamic columns whose IDs are NOT in the incoming set
|
|
1203
|
+
// (if the plugin now includes a column that was previously dynamic,
|
|
1204
|
+
// the plugin's version takes precedence)
|
|
1205
|
+
const dynamicToKeep = existingDynamic.filter(
|
|
1206
|
+
(col) => !incomingIds.has(col.id)
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
// Merge: plugin columns + retained dynamic columns
|
|
1210
|
+
project.columns = [...columns, ...dynamicToKeep];
|
|
1211
|
+
|
|
1212
|
+
// Re-normalize order values to account for merged columns
|
|
1213
|
+
renormalizeColumnOrders(project);
|
|
1214
|
+
|
|
717
1215
|
this.schedulePersist();
|
|
718
1216
|
}
|
|
719
1217
|
|
|
1218
|
+
// Recompute column visibility (columns changed)
|
|
1219
|
+
this.broadcastColumnsUpdate(project);
|
|
1220
|
+
|
|
720
1221
|
return {
|
|
721
1222
|
event: "columns:update",
|
|
722
1223
|
data: {
|
|
723
1224
|
...data,
|
|
724
1225
|
projectPath: project.projectPath,
|
|
725
1226
|
columns: project.columns,
|
|
1227
|
+
visibleColumns: this.computeVisibleColumns(project),
|
|
726
1228
|
},
|
|
727
1229
|
};
|
|
728
1230
|
}
|
|
@@ -827,6 +1329,8 @@ export class StateManager {
|
|
|
827
1329
|
// Mark all projects as disconnected on load (plugins will re-register)
|
|
828
1330
|
for (const [, project] of this.state.projects) {
|
|
829
1331
|
project.connected = false;
|
|
1332
|
+
// Initialize empty activeAgents Set for each loaded project
|
|
1333
|
+
this.activeAgents.set(project.projectPath, new Set());
|
|
830
1334
|
// No session running after restart — mark active pipelines as idle
|
|
831
1335
|
for (const [, pipeline] of project.pipelines) {
|
|
832
1336
|
if (pipeline.status === "active") {
|
|
@@ -923,11 +1427,24 @@ export class StateManager {
|
|
|
923
1427
|
destroy(): void {
|
|
924
1428
|
this.persistNow();
|
|
925
1429
|
this.listeners = [];
|
|
1430
|
+
// Clear all pipeline hide timers
|
|
1431
|
+
for (const timer of this.pipelineHideTimers.values()) {
|
|
1432
|
+
clearTimeout(timer);
|
|
1433
|
+
}
|
|
1434
|
+
this.pipelineHideTimers.clear();
|
|
1435
|
+
this._lastVisibleColumnsKey.clear();
|
|
926
1436
|
}
|
|
927
1437
|
|
|
928
1438
|
/** Reset all state (for testing) */
|
|
929
1439
|
clear(): void {
|
|
930
1440
|
this.state = { projects: new Map() };
|
|
1441
|
+
this.activeAgents.clear();
|
|
1442
|
+
// Clear all pipeline hide timers
|
|
1443
|
+
for (const timer of this.pipelineHideTimers.values()) {
|
|
1444
|
+
clearTimeout(timer);
|
|
1445
|
+
}
|
|
1446
|
+
this.pipelineHideTimers.clear();
|
|
1447
|
+
this._lastVisibleColumnsKey.clear();
|
|
931
1448
|
if (this.persistDebounceTimer) {
|
|
932
1449
|
clearTimeout(this.persistDebounceTimer);
|
|
933
1450
|
this.persistDebounceTimer = null;
|