agent-relay-server 0.119.6 → 0.119.9
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/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/{MessageBubble-Azda2GIf.js → MessageBubble-U5sX3SNZ.js} +2 -2
- package/public/assets/{MessageBubble-Azda2GIf.js.map → MessageBubble-U5sX3SNZ.js.map} +1 -1
- package/public/assets/PlanGraphPanel-Bi2gvXeE.js +2 -0
- package/public/assets/PlanGraphPanel-Bi2gvXeE.js.map +1 -0
- package/public/assets/{activity-B0hz4XPy.js → activity-Cwkz51pG.js} +2 -2
- package/public/assets/{activity-B0hz4XPy.js.map → activity-Cwkz51pG.js.map} +1 -1
- package/public/assets/{agent-profiles-BF0oSOTq.js → agent-profiles-BgddVqRp.js} +2 -2
- package/public/assets/{agent-profiles-BF0oSOTq.js.map → agent-profiles-BgddVqRp.js.map} +1 -1
- package/public/assets/{agents-CW4--j3Q.js → agents-CiGYBw0h.js} +2 -2
- package/public/assets/{agents-CW4--j3Q.js.map → agents-CiGYBw0h.js.map} +1 -1
- package/public/assets/{automation-S6_bQ2t-.js → automation-BBJB4RaE.js} +2 -2
- package/public/assets/{automation-S6_bQ2t-.js.map → automation-BBJB4RaE.js.map} +1 -1
- package/public/assets/{chat-hoFhhvgy.js → chat-DS578gZy.js} +2 -2
- package/public/assets/{chat-hoFhhvgy.js.map → chat-DS578gZy.js.map} +1 -1
- package/public/assets/{formatted-body-BHm145gP.js → formatted-body-DYUVqWym.js} +3 -3
- package/public/assets/{formatted-body-BHm145gP.js.map → formatted-body-DYUVqWym.js.map} +1 -1
- package/public/assets/{formatted-body-impl-CvE31j9F.js → formatted-body-impl-DdcyxVO0.js} +2 -2
- package/public/assets/{formatted-body-impl-CvE31j9F.js.map → formatted-body-impl-DdcyxVO0.js.map} +1 -1
- package/public/assets/{index-TamtghSr.js → index-CqcAqQKW.js} +6 -6
- package/public/assets/{index-TamtghSr.js.map → index-CqcAqQKW.js.map} +1 -1
- package/public/assets/{insights-Ci9ftVfl.js → insights-BunHbsTA.js} +2 -2
- package/public/assets/{insights-Ci9ftVfl.js.map → insights-BunHbsTA.js.map} +1 -1
- package/public/assets/{maintenance-bVhNbr33.js → maintenance-cICBVcpJ.js} +2 -2
- package/public/assets/{maintenance-bVhNbr33.js.map → maintenance-cICBVcpJ.js.map} +1 -1
- package/public/assets/{managed-agents-D5HO4dhI.js → managed-agents-BK9_Gi-M.js} +2 -2
- package/public/assets/{managed-agents-D5HO4dhI.js.map → managed-agents-BK9_Gi-M.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DO-p83dz.js → markdown-preview-impl-EZfcTHG9.js} +2 -2
- package/public/assets/{markdown-preview-impl-DO-p83dz.js.map → markdown-preview-impl-EZfcTHG9.js.map} +1 -1
- package/public/assets/{memory-D72o8umQ.js → memory-Vi-8HvK3.js} +2 -2
- package/public/assets/{memory-D72o8umQ.js.map → memory-Vi-8HvK3.js.map} +1 -1
- package/public/assets/{messages-5L_Bc_9w.js → messages-DScIiBMI.js} +2 -2
- package/public/assets/{messages-5L_Bc_9w.js.map → messages-DScIiBMI.js.map} +1 -1
- package/public/assets/{orchestrators-CnfgkQpl.js → orchestrators-DLogldXx.js} +2 -2
- package/public/assets/{orchestrators-CnfgkQpl.js.map → orchestrators-DLogldXx.js.map} +1 -1
- package/public/assets/{overview-Bu39NIyR.js → overview-B_RQAQR8.js} +2 -2
- package/public/assets/{overview-Bu39NIyR.js.map → overview-B_RQAQR8.js.map} +1 -1
- package/public/assets/{pairs-WuamP5W5.js → pairs-CSiPQ7E2.js} +2 -2
- package/public/assets/{pairs-WuamP5W5.js.map → pairs-CSiPQ7E2.js.map} +1 -1
- package/public/assets/{prompt-settings-BCrQreU6.js → prompt-settings-DsDi_Xpd.js} +2 -2
- package/public/assets/{prompt-settings-BCrQreU6.js.map → prompt-settings-DsDi_Xpd.js.map} +1 -1
- package/public/assets/{registry-kHKn0xwg.js → registry-DHFUWIdb.js} +2 -2
- package/public/assets/{registry-kHKn0xwg.js.map → registry-DHFUWIdb.js.map} +1 -1
- package/public/assets/{security-Dk5t3r_K.js → security-D2nHkJH6.js} +2 -2
- package/public/assets/{security-Dk5t3r_K.js.map → security-D2nHkJH6.js.map} +1 -1
- package/public/assets/{select-BdsZQfU2.js → select-BjNcommA.js} +2 -2
- package/public/assets/{select-BdsZQfU2.js.map → select-BjNcommA.js.map} +1 -1
- package/public/assets/{settings-EpjzQHnZ.js → settings-DiUEq54n.js} +2 -2
- package/public/assets/{settings-EpjzQHnZ.js.map → settings-DiUEq54n.js.map} +1 -1
- package/public/assets/{tasks-CedMQHbg.js → tasks-Dx8GVQiR.js} +2 -2
- package/public/assets/{tasks-CedMQHbg.js.map → tasks-Dx8GVQiR.js.map} +1 -1
- package/public/assets/{teams-BlHBo5Qt.js → teams-DQtf7bha.js} +2 -2
- package/public/assets/{teams-BlHBo5Qt.js.map → teams-DQtf7bha.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-gGqYpsjq.js → terminal-viewer-impl-D5rhXV4u.js} +2 -2
- package/public/assets/{terminal-viewer-impl-gGqYpsjq.js.map → terminal-viewer-impl-D5rhXV4u.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-D7opMqtU.js → work-queue-BfWNDp7U.js} +2 -2
- package/public/assets/{work-queue-D7opMqtU.js.map → work-queue-BfWNDp7U.js.map} +1 -1
- package/public/index.html +2 -2
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/commands-db.ts +72 -20
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +7 -0
- package/src/db/orchestrators.ts +1 -1
- package/src/db/provider-quotas.ts +2 -34
- package/src/db/schema.ts +2 -0
- package/src/db/work-evidence.ts +38 -0
- package/src/db/workspaces.ts +48 -3
- package/src/lifecycle-manager.ts +2 -4
- package/src/maintenance/jobs.ts +10 -1
- package/src/maintenance/workspaces.ts +71 -13
- package/src/routes/commands.ts +20 -4
- package/src/routes/workspaces.ts +6 -11
- package/src/services/shutdown-agent.ts +6 -1
- package/src/services/spawn-agent.ts +50 -7
- package/src/services/transient-agent-reaper.ts +82 -23
- package/src/workspace-actions.ts +26 -16
- package/src/workspace-auto-merge.ts +95 -13
- package/src/workspace-host.ts +55 -0
- package/src/workspace-merge.ts +3 -4
- package/src/workspace-orphans.ts +78 -55
- package/src/workspace-probe.ts +2 -3
- package/public/assets/PlanGraphPanel-B9TN_KQr.js +0 -2
- package/public/assets/PlanGraphPanel-B9TN_KQr.js.map +0 -1
package/src/workspace-actions.ts
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
// the Command and the caller emits via its own `emitCommand` (kept module-local).
|
|
11
11
|
|
|
12
12
|
import { createCommand } from "./commands-db";
|
|
13
|
-
import {
|
|
13
|
+
import { workspaceOwnerOrchestrator } from "./workspace-host";
|
|
14
14
|
import {
|
|
15
15
|
createActivityEvent,
|
|
16
16
|
getWorkspace,
|
|
17
|
+
claimWorkspaceMetadata,
|
|
17
18
|
listOrchestrators,
|
|
18
19
|
patchWorkspaceMetadata,
|
|
20
|
+
releaseWorkspaceClaimMetadata,
|
|
19
21
|
updateWorkspaceStatus,
|
|
20
22
|
} from "./db";
|
|
21
23
|
import { emitActivityEvent } from "./sse";
|
|
@@ -148,8 +150,17 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
|
|
|
148
150
|
// so deterministic landing can't race a steward mid-validation. No status change.
|
|
149
151
|
if (action === "claim" || action === "release-claim") {
|
|
150
152
|
const release = action === "release-claim";
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
+
const claimant = agentId ?? "steward";
|
|
154
|
+
const result = release
|
|
155
|
+
? releaseWorkspaceClaimMetadata(workspace.id, claimant, claimMetadataPatch(true), Date.now())
|
|
156
|
+
: claimWorkspaceMetadata(workspace.id, claimant, claimMetadataPatch(false, claimant, input.purpose), Date.now());
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
if (result.reason === "not-found") return { ok: false, httpStatus: 404, error: "workspace not found" };
|
|
159
|
+
const active = result.workspace ? workspaceActiveClaim(result.workspace) : null;
|
|
160
|
+
const owner = active?.by ? ` by ${active.by}` : "";
|
|
161
|
+
return { ok: false, httpStatus: 409, error: `workspace ${workspace.id} is already claimed${owner}` };
|
|
162
|
+
}
|
|
163
|
+
const updated = result.workspace;
|
|
153
164
|
recordWorkspaceAudit({
|
|
154
165
|
action,
|
|
155
166
|
workspace: updated,
|
|
@@ -231,6 +242,14 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
|
|
|
231
242
|
};
|
|
232
243
|
}
|
|
233
244
|
|
|
245
|
+
let command: Command | undefined;
|
|
246
|
+
if (requiresCommand) {
|
|
247
|
+
// Only `cleanup` reaches here — `merge` returned early via the shared helper.
|
|
248
|
+
const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
|
|
249
|
+
if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
|
|
250
|
+
command = built.command;
|
|
251
|
+
}
|
|
252
|
+
|
|
234
253
|
const updated = updateWorkspaceStatus(workspace.id, nextStatus, {
|
|
235
254
|
...metadata,
|
|
236
255
|
...(detail ? { detail } : {}),
|
|
@@ -250,13 +269,6 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
|
|
|
250
269
|
// + lease-serialized; a non-landable row is a harmless no-op (autoMergeForRepo re-checks).
|
|
251
270
|
if (READY_TO_LAND_STATUSES.has(nextStatus)) scheduleRepoMerge(updated.repoRoot);
|
|
252
271
|
|
|
253
|
-
let command: Command | undefined;
|
|
254
|
-
if (requiresCommand) {
|
|
255
|
-
// Only `cleanup` reaches here — `merge` returned early via the shared helper.
|
|
256
|
-
const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
|
|
257
|
-
if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
|
|
258
|
-
command = built.command;
|
|
259
|
-
}
|
|
260
272
|
recordWorkspaceAudit({
|
|
261
273
|
action,
|
|
262
274
|
workspace: updated,
|
|
@@ -289,9 +301,8 @@ export function buildWorkspaceCleanupCommand(
|
|
|
289
301
|
error: `workspace ${workspace.id} owner is still online; pass force:true to clean it up intentionally`,
|
|
290
302
|
};
|
|
291
303
|
}
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
if (!owner) return { ok: false, status: 409, error: "no orchestrator owns this workspace path; use DELETE /api/workspaces/:id to purge the record" };
|
|
304
|
+
const owner = workspaceOwnerOrchestrator(workspace);
|
|
305
|
+
if (!owner) return { ok: false, status: 409, error: "no online orchestrator owns this workspace path; retry when the host is online or use DELETE /api/workspaces/:id to purge the record" };
|
|
295
306
|
const command = createCommand({
|
|
296
307
|
type: "workspace.cleanup",
|
|
297
308
|
source: "system",
|
|
@@ -308,7 +319,7 @@ export function buildWorkspaceCleanupCommand(
|
|
|
308
319
|
requestedBy,
|
|
309
320
|
requestedAt: Date.now(),
|
|
310
321
|
deleteBranch: true,
|
|
311
|
-
queued:
|
|
322
|
+
queued: false,
|
|
312
323
|
},
|
|
313
324
|
});
|
|
314
325
|
return { ok: true, command };
|
|
@@ -321,8 +332,7 @@ function buildWorkspaceDepsRefreshCommand(
|
|
|
321
332
|
requestedBy: string,
|
|
322
333
|
checkOnly: boolean,
|
|
323
334
|
): { ok: true; command: Command } | { ok: false; status: number; error: string } {
|
|
324
|
-
const
|
|
325
|
-
const owner = owners.find((candidate) => candidate.status === "online") ?? owners[0];
|
|
335
|
+
const owner = workspaceOwnerOrchestrator(workspace, listOrchestrators(), { onlineOnly: false });
|
|
326
336
|
if (!owner) return { ok: false, status: 409, error: "no online orchestrator owns this workspace path" };
|
|
327
337
|
const command = createCommand({
|
|
328
338
|
type: "workspace.deps-refresh",
|
|
@@ -154,6 +154,8 @@ export const MAX_NO_PROGRESS_MERGE_ATTEMPTS = 3;
|
|
|
154
154
|
|
|
155
155
|
// Don't re-escalate a backed-off lane more than once per this window.
|
|
156
156
|
const NO_PROGRESS_ESCALATION_COOLDOWN_MS = 30 * 60 * 1000;
|
|
157
|
+
export const AUTO_MERGE_ERROR_BACKOFF_MS = 3_000;
|
|
158
|
+
export const AUTO_MERGE_WATCHDOG_STALE_MS = 5 * 60 * 1000;
|
|
157
159
|
|
|
158
160
|
/** The recorded count of consecutive no-progress merge attempts for a workspace (#638). */
|
|
159
161
|
export function mergeNoProgressCount(metadata: Record<string, unknown>): number {
|
|
@@ -377,30 +379,105 @@ async function landCandidates(
|
|
|
377
379
|
// in flight for the repo just flags a rerun, so concurrent readies don't fan out
|
|
378
380
|
// redundant host probes. Errors are swallowed: the runOnStart backstop is the
|
|
379
381
|
// self-healer for anything a dropped run strands.
|
|
380
|
-
|
|
382
|
+
type RepoMergeRunState = {
|
|
383
|
+
rerun: boolean;
|
|
384
|
+
lastProgressAt: number;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const repoMergeRunning = new Map<string, RepoMergeRunState>();
|
|
388
|
+
const repoMergeLastCompletedAt = new Map<string, number>();
|
|
389
|
+
let autoMergeWatchdogStartedAt = Date.now();
|
|
390
|
+
|
|
391
|
+
function errMessage(error: unknown): string {
|
|
392
|
+
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
393
|
+
return String(error);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function landableWorkspaces(repoRoot?: string): WorkspaceRecord[] {
|
|
397
|
+
return listWorkspaces(repoRoot ? { repoRoot } : undefined).filter(
|
|
398
|
+
(ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && READY_TO_LAND_STATUSES.has(ws.status),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function noteAutoMergeCycleCompleted(repoRoot: string): void {
|
|
403
|
+
const completedAt = Date.now();
|
|
404
|
+
repoMergeLastCompletedAt.set(repoRoot, completedAt);
|
|
405
|
+
const state = repoMergeRunning.get(repoRoot);
|
|
406
|
+
if (state) state.lastProgressAt = completedAt;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function sleep(ms: number): Promise<void> {
|
|
410
|
+
await new Promise<void>((resolve) => {
|
|
411
|
+
const timer = setTimeout(resolve, ms);
|
|
412
|
+
timer.unref?.();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
381
415
|
|
|
382
416
|
export function scheduleRepoMerge(repoRoot: string | undefined): void {
|
|
383
417
|
if (!repoRoot) return;
|
|
384
418
|
if (!workspaceAutoMergeEnabled()) return;
|
|
385
419
|
const existing = repoMergeRunning.get(repoRoot);
|
|
386
420
|
if (existing) { existing.rerun = true; return; }
|
|
387
|
-
const state = { rerun: false };
|
|
421
|
+
const state: RepoMergeRunState = { rerun: false, lastProgressAt: Date.now() };
|
|
388
422
|
repoMergeRunning.set(repoRoot, state);
|
|
389
423
|
void (async () => {
|
|
424
|
+
let retriedAfterError = false;
|
|
390
425
|
try {
|
|
391
426
|
do {
|
|
392
427
|
state.rerun = false;
|
|
393
|
-
|
|
428
|
+
state.lastProgressAt = Date.now();
|
|
429
|
+
try {
|
|
430
|
+
await autoMergeForRepo(repoRoot);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
console.warn(`[workspace-auto-merge] Repo auto-merge cycle failed for ${repoRoot}: ${errMessage(err)}`);
|
|
433
|
+
if (!retriedAfterError) {
|
|
434
|
+
retriedAfterError = true;
|
|
435
|
+
await sleep(AUTO_MERGE_ERROR_BACKOFF_MS);
|
|
436
|
+
state.rerun = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
394
439
|
} while (state.rerun);
|
|
395
440
|
} catch {
|
|
396
441
|
// Swallow — the periodic backstop (runOnStart + interval) is the self-healer.
|
|
397
442
|
} finally {
|
|
398
|
-
repoMergeRunning.delete(repoRoot);
|
|
443
|
+
if (repoMergeRunning.get(repoRoot) === state) repoMergeRunning.delete(repoRoot);
|
|
399
444
|
}
|
|
400
445
|
})();
|
|
401
446
|
}
|
|
402
447
|
|
|
403
|
-
export function resetRepoMergeSchedulerForTests(): void {
|
|
448
|
+
export function resetRepoMergeSchedulerForTests(): void {
|
|
449
|
+
repoMergeRunning.clear();
|
|
450
|
+
repoMergeLastCompletedAt.clear();
|
|
451
|
+
autoMergeWatchdogStartedAt = Date.now();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function runAutoMergeWatchdog(
|
|
455
|
+
opts: { now?: number; staleMs?: number; log?: (message: string) => void } = {},
|
|
456
|
+
): Record<string, unknown> {
|
|
457
|
+
const now = opts.now ?? Date.now();
|
|
458
|
+
const staleMs = opts.staleMs ?? AUTO_MERGE_WATCHDOG_STALE_MS;
|
|
459
|
+
const ready = landableWorkspaces();
|
|
460
|
+
const repoRoots = [...new Set(ready.map((ws) => ws.repoRoot).filter(Boolean))];
|
|
461
|
+
const repoStaleForMs = Object.fromEntries(repoRoots.map((repoRoot) => {
|
|
462
|
+
const state = repoMergeRunning.get(repoRoot);
|
|
463
|
+
const lastProgressAt = state?.lastProgressAt ?? repoMergeLastCompletedAt.get(repoRoot) ?? autoMergeWatchdogStartedAt;
|
|
464
|
+
return [repoRoot, now - lastProgressAt];
|
|
465
|
+
}));
|
|
466
|
+
const staleForMs = Object.values(repoStaleForMs).reduce((max, value) => Math.max(max, value), 0);
|
|
467
|
+
if (!ready.length) return { readyCount: 0, staleForMs, repoStaleForMs, restartedRepoRoots: [] };
|
|
468
|
+
|
|
469
|
+
const restartedRepoRoots = repoRoots.filter((repoRoot) => repoStaleForMs[repoRoot]! >= staleMs);
|
|
470
|
+
if (!restartedRepoRoots.length) return { readyCount: ready.length, staleForMs, repoStaleForMs, restartedRepoRoots: [] };
|
|
471
|
+
|
|
472
|
+
const log = opts.log ?? console.warn;
|
|
473
|
+
log(`[workspace-auto-merge] Watchdog: no auto-merge cycle completed for ${staleForMs}ms while ${ready.length} ready workspace(s) exist; restarting ${restartedRepoRoots.length} repo merge loop(s)`);
|
|
474
|
+
for (const repoRoot of restartedRepoRoots) {
|
|
475
|
+
const existing = repoMergeRunning.get(repoRoot);
|
|
476
|
+
if (existing) existing.rerun = true;
|
|
477
|
+
else scheduleRepoMerge(repoRoot);
|
|
478
|
+
}
|
|
479
|
+
return { readyCount: ready.length, staleForMs, repoStaleForMs, restartedRepoRoots };
|
|
480
|
+
}
|
|
404
481
|
|
|
405
482
|
// Drain the landable rows for ONE repo (the event-driven fast path). Scoped to the
|
|
406
483
|
// repo whose ready/settle event fired so it doesn't re-probe every other repo's worktrees.
|
|
@@ -408,10 +485,12 @@ async function autoMergeForRepo(repoRoot: string): Promise<LandSweepResult> {
|
|
|
408
485
|
if (!workspaceAutoMergeEnabled()) return emptyLandSweep();
|
|
409
486
|
const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
|
|
410
487
|
if (!orchestrators.length) return emptyLandSweep();
|
|
411
|
-
const candidates =
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
488
|
+
const candidates = landableWorkspaces(repoRoot);
|
|
489
|
+
try {
|
|
490
|
+
return await landCandidates(candidates, orchestrators);
|
|
491
|
+
} finally {
|
|
492
|
+
noteAutoMergeCycleCompleted(repoRoot);
|
|
493
|
+
}
|
|
415
494
|
}
|
|
416
495
|
|
|
417
496
|
// Self-healing backstop (#237): the periodic resync (workspace-auto-merge maintenance
|
|
@@ -424,10 +503,13 @@ export async function autoMergeCleanFastForwards(): Promise<Record<string, unkno
|
|
|
424
503
|
const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
|
|
425
504
|
if (!orchestrators.length) return { scanned: 0, skipped: "no online orchestrators" };
|
|
426
505
|
|
|
427
|
-
const candidates =
|
|
428
|
-
|
|
429
|
-
);
|
|
506
|
+
const candidates = landableWorkspaces();
|
|
507
|
+
const repoRoots = [...new Set(candidates.map((ws) => ws.repoRoot).filter(Boolean))];
|
|
430
508
|
// Spread to a plain Record for the maintenance-job result contract (the named
|
|
431
509
|
// LandSweepResult interface has no index signature).
|
|
432
|
-
|
|
510
|
+
try {
|
|
511
|
+
return { ...(await landCandidates(candidates, orchestrators)) };
|
|
512
|
+
} finally {
|
|
513
|
+
for (const repoRoot of repoRoots) noteAutoMergeCycleCompleted(repoRoot);
|
|
514
|
+
}
|
|
433
515
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Command, Orchestrator, WorkspaceRecord } from "./types";
|
|
2
|
+
import { getOrchestrator, listOrchestrators } from "./db";
|
|
3
|
+
import { isPathWithinBase } from "./utils";
|
|
4
|
+
|
|
5
|
+
interface WorkspaceHost {
|
|
6
|
+
id: string;
|
|
7
|
+
status?: string;
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
baseDir?: string;
|
|
10
|
+
agentId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface WorkspaceHostOptions {
|
|
14
|
+
onlineOnly?: boolean;
|
|
15
|
+
requireApiUrl?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hostUsable<T extends WorkspaceHost>(orch: T | undefined, opts: WorkspaceHostOptions): orch is T {
|
|
19
|
+
if (!orch) return false;
|
|
20
|
+
if (opts.onlineOnly !== false && orch.status !== "online") return false;
|
|
21
|
+
if (opts.requireApiUrl && !orch.apiUrl) return false;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function workspaceOwnerOrchestrators<T extends WorkspaceHost = Orchestrator>(
|
|
26
|
+
workspace: WorkspaceRecord,
|
|
27
|
+
orchestrators?: T[],
|
|
28
|
+
opts: WorkspaceHostOptions = {},
|
|
29
|
+
): T[] {
|
|
30
|
+
const hosts = orchestrators ?? (listOrchestrators() as unknown as T[]);
|
|
31
|
+
if (workspace.orchestratorId) {
|
|
32
|
+
const owner = hosts.find((candidate) => candidate.id === workspace.orchestratorId);
|
|
33
|
+
return hostUsable(owner, opts) ? [owner] : [];
|
|
34
|
+
}
|
|
35
|
+
return hosts.filter((candidate) =>
|
|
36
|
+
hostUsable(candidate, opts) && Boolean(candidate.baseDir) && isPathWithinBase(workspace.sourceCwd, candidate.baseDir!),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function workspaceOwnerOrchestrator<T extends WorkspaceHost = Orchestrator>(
|
|
41
|
+
workspace: WorkspaceRecord,
|
|
42
|
+
orchestrators?: T[],
|
|
43
|
+
opts: WorkspaceHostOptions = {},
|
|
44
|
+
): T | undefined {
|
|
45
|
+
return workspaceOwnerOrchestrators(workspace, orchestrators, opts)[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function commandTargetsWorkspaceOwner(command: Command, workspace: WorkspaceRecord): boolean {
|
|
49
|
+
const ownerId = workspace.orchestratorId;
|
|
50
|
+
if (!ownerId) return true;
|
|
51
|
+
const owner = getOrchestrator(ownerId);
|
|
52
|
+
return command.target === ownerId
|
|
53
|
+
|| command.target === `orchestrator-${ownerId}`
|
|
54
|
+
|| (owner?.agentId ? command.target === owner.agentId : false);
|
|
55
|
+
}
|
package/src/workspace-merge.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from "./types";
|
|
23
23
|
import { AUTO_MERGE_POLICIES } from "./types";
|
|
24
24
|
import { isPathWithinBase } from "./utils";
|
|
25
|
+
import { workspaceOwnerOrchestrators } from "./workspace-host";
|
|
25
26
|
|
|
26
27
|
// Re-export so consumers can import the tuple from the strategy home without a
|
|
27
28
|
// second import hop.
|
|
@@ -164,11 +165,9 @@ export function workspaceMergeOrchestrators(
|
|
|
164
165
|
workspace: WorkspaceRecord,
|
|
165
166
|
orchestrators: Orchestrator[] = listOrchestrators(),
|
|
166
167
|
): Orchestrator[] {
|
|
167
|
-
const candidates = orchestrators
|
|
168
|
-
candidate.status === "online" && isPathWithinBase(workspace.sourceCwd, candidate.baseDir),
|
|
169
|
-
);
|
|
168
|
+
const candidates = workspaceOwnerOrchestrators(workspace, orchestrators);
|
|
170
169
|
const ownerKeys = workspaceOwnerHostKeys(workspace);
|
|
171
|
-
if (ownerKeys.size === 0) return candidates;
|
|
170
|
+
if (workspace.orchestratorId || ownerKeys.size === 0) return candidates;
|
|
172
171
|
return [...candidates].sort((a, b) => {
|
|
173
172
|
const aOwner = ownerKeys.has(a.id) || ownerKeys.has(a.hostname) || ownerKeys.has(a.agentId) ? 0 : 1;
|
|
174
173
|
const bOwner = ownerKeys.has(b.id) || ownerKeys.has(b.hostname) || ownerKeys.has(b.agentId) ? 0 : 1;
|
package/src/workspace-orphans.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { isPathWithinBase } from "./utils";
|
|
|
28
28
|
import { TERMINAL_WORKSPACE_STATUSES, worktreeReapable, type WorktreeReapState } from "./workspace-phase";
|
|
29
29
|
import { isOwnerAlive, liveAgentCwds, worktreeInUseByLiveAgent } from "./workspace-merge";
|
|
30
30
|
import { applyWorkspaceAction } from "./workspace-actions";
|
|
31
|
+
import { workspaceOwnerOrchestrator } from "./workspace-host";
|
|
31
32
|
import { UNLANDED_FLAG_COOLDOWN_MS, orphanGraceMs, orphanWorktreeReapEnabled, relayToken } from "./config";
|
|
32
33
|
|
|
33
34
|
// Don't re-flag the same un-landed orphan every sweep — surface it once, then
|
|
@@ -139,6 +140,17 @@ function knownRepoRoots(workspaces: WorkspaceRecord[]): string[] {
|
|
|
139
140
|
return [...new Set(workspaces.map((ws) => ws.repoRoot).filter(Boolean))];
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
function repoProbeHosts(repoRoot: string, workspaces: WorkspaceRecord[], orchestrators: OnlineOrchestrator[]): OnlineOrchestrator[] {
|
|
144
|
+
const ownerIds = new Set(
|
|
145
|
+
workspaces
|
|
146
|
+
.filter((ws) => ws.orchestratorId && resolve(ws.repoRoot) === resolve(repoRoot))
|
|
147
|
+
.map((ws) => ws.orchestratorId!),
|
|
148
|
+
);
|
|
149
|
+
if (ownerIds.size > 0) return orchestrators.filter((candidate) => ownerIds.has(candidate.id));
|
|
150
|
+
const legacy = orchestrators.find((candidate) => isPathWithinBase(repoRoot, candidate.baseDir));
|
|
151
|
+
return legacy ? [legacy] : [];
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
interface CollectOrphansResult {
|
|
143
155
|
orphans: WorkspaceOrphan[];
|
|
144
156
|
/** Live isolated rows whose worktree is missing on disk (DB→disk drift). */
|
|
@@ -196,64 +208,72 @@ export async function collectWorkspaceOrphans(): Promise<CollectOrphansResult> {
|
|
|
196
208
|
// (their worktree lives under a different repo's probe than the one iterating). #278
|
|
197
209
|
const onDiskAll = new Set<string>();
|
|
198
210
|
const probedRepoRoots = new Set<string>();
|
|
211
|
+
const onDiskByOwner = new Set<string>();
|
|
212
|
+
const probedOwnerRepoRoots = new Set<string>();
|
|
199
213
|
|
|
200
214
|
for (const repoRoot of knownRepoRoots(all)) {
|
|
201
|
-
const orch
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const worktree of probe.worktrees) {
|
|
213
|
-
if (!worktree.path || resolve(worktree.path) === resolve(repoRoot)) continue;
|
|
214
|
-
// Only agent-relay-created worktrees (agent/* branches) are reclaimable —
|
|
215
|
-
// never touch a user's own linked worktrees.
|
|
216
|
-
if (!worktree.branch?.startsWith("agent/")) continue;
|
|
217
|
-
const row = rowsByPath.get(resolve(worktree.path));
|
|
218
|
-
if (row && !TERMINAL_WORKSPACE_STATUSES.has(row.status)) {
|
|
219
|
-
if (row.mode === "isolated") {
|
|
220
|
-
deadOwnerWorkspaces.push({
|
|
221
|
-
workspaceId: row.id,
|
|
222
|
-
worktreePath: worktree.path,
|
|
223
|
-
repoRoot: row.repoRoot,
|
|
224
|
-
branch: row.branch ?? worktree.branch,
|
|
225
|
-
baseRef: row.baseRef,
|
|
226
|
-
baseSha: row.baseSha,
|
|
227
|
-
ownerAgentId: row.ownerAgentId,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
continue; // tracked & live — not an orphan
|
|
215
|
+
for (const orch of repoProbeHosts(repoRoot, all, orchestrators)) {
|
|
216
|
+
if (!orch.apiUrl) continue;
|
|
217
|
+
const probe = await fetchHostProbe(orch.apiUrl, repoRoot);
|
|
218
|
+
if (!probe?.worktrees) continue;
|
|
219
|
+
|
|
220
|
+
probedRepoRoots.add(resolve(repoRoot));
|
|
221
|
+
probedOwnerRepoRoots.add(`${orch.id}:${resolve(repoRoot)}`);
|
|
222
|
+
for (const wt of probe.worktrees) {
|
|
223
|
+
if (!wt.path) continue;
|
|
224
|
+
onDiskAll.add(resolve(wt.path));
|
|
225
|
+
onDiskByOwner.add(`${orch.id}:${resolve(wt.path)}`);
|
|
231
226
|
}
|
|
232
227
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
228
|
+
// disk→DB drift: a worktree on disk with no live row → orphan. A worktree with a
|
|
229
|
+
// live isolated row is NOT an orphan, but if its owner has died it's the #279
|
|
230
|
+
// dead-owner case — surface it for the reaper to evaluate (liveness + grace).
|
|
231
|
+
for (const worktree of probe.worktrees) {
|
|
232
|
+
if (!worktree.path || resolve(worktree.path) === resolve(repoRoot)) continue;
|
|
233
|
+
// Only agent-relay-created worktrees (agent/* branches) are reclaimable —
|
|
234
|
+
// never touch a user's own linked worktrees.
|
|
235
|
+
if (!worktree.branch?.startsWith("agent/")) continue;
|
|
236
|
+
const row = rowsByPath.get(resolve(worktree.path));
|
|
237
|
+
if (row && !TERMINAL_WORKSPACE_STATUSES.has(row.status)) {
|
|
238
|
+
if (row.mode === "isolated") {
|
|
239
|
+
deadOwnerWorkspaces.push({
|
|
240
|
+
workspaceId: row.id,
|
|
241
|
+
worktreePath: worktree.path,
|
|
242
|
+
repoRoot: row.repoRoot,
|
|
243
|
+
branch: row.branch ?? worktree.branch,
|
|
244
|
+
baseRef: row.baseRef,
|
|
245
|
+
baseSha: row.baseSha,
|
|
246
|
+
ownerAgentId: row.ownerAgentId,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
continue; // tracked & live — not an orphan
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const orphan: WorkspaceOrphan = {
|
|
253
|
+
worktreePath: worktree.path,
|
|
254
|
+
repoRoot,
|
|
255
|
+
branch: worktree.branch,
|
|
256
|
+
headSha: worktree.headSha,
|
|
257
|
+
baseRef: probe.branch,
|
|
258
|
+
hadTerminalRow: Boolean(row),
|
|
248
259
|
};
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
const preview = await fetchWorktreeReapState(orch.apiUrl, worktree.path, probe.branch);
|
|
261
|
+
if (preview && !preview.missing && !preview.error) {
|
|
262
|
+
const state: WorktreeReapState = {
|
|
263
|
+
landed: preview.landed,
|
|
264
|
+
ahead: preview.ahead,
|
|
265
|
+
unmergedAhead: preview.unmergedAhead,
|
|
266
|
+
dirtyCount: preview.dirtyCount,
|
|
267
|
+
};
|
|
268
|
+
orphan.landed = preview.landed;
|
|
269
|
+
orphan.ahead = preview.ahead;
|
|
270
|
+
orphan.unmergedAhead = preview.unmergedAhead;
|
|
271
|
+
orphan.dirty = (preview.dirtyCount ?? 0) > 0;
|
|
272
|
+
orphan.safeToReap = worktreeReapable(state);
|
|
273
|
+
}
|
|
274
|
+
// No probe → safeToReap stays undefined (treated as not-safe by callers).
|
|
275
|
+
orphans.push(orphan);
|
|
254
276
|
}
|
|
255
|
-
// No probe → safeToReap stays undefined (treated as not-safe by callers).
|
|
256
|
-
orphans.push(orphan);
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
|
|
@@ -268,7 +288,10 @@ export async function collectWorkspaceOrphans(): Promise<CollectOrphansResult> {
|
|
|
268
288
|
if (ws.mode !== "isolated" || !ws.worktreePath) continue;
|
|
269
289
|
if (TERMINAL_WORKSPACE_STATUSES.has(ws.status)) continue;
|
|
270
290
|
if (onDiskAll.has(resolve(ws.worktreePath))) continue;
|
|
271
|
-
const coverable =
|
|
291
|
+
const coverable = ws.orchestratorId
|
|
292
|
+
? probedOwnerRepoRoots.has(`${ws.orchestratorId}:${resolve(ws.repoRoot)}`)
|
|
293
|
+
|| onDiskByOwner.has(`${ws.orchestratorId}:${resolve(ws.repoRoot)}`)
|
|
294
|
+
: probedRepoRoots.has(resolve(ws.repoRoot)) || onDiskAll.has(resolve(ws.repoRoot));
|
|
272
295
|
if (!coverable) continue;
|
|
273
296
|
missingWorktrees.push({
|
|
274
297
|
workspaceId: ws.id,
|
|
@@ -420,7 +443,7 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
|
|
|
420
443
|
continue;
|
|
421
444
|
}
|
|
422
445
|
|
|
423
|
-
const orch =
|
|
446
|
+
const orch = workspaceOwnerOrchestrator(workspace, orchestrators, { onlineOnly: false, requireApiUrl: true });
|
|
424
447
|
const probe = orch?.apiUrl
|
|
425
448
|
? await fetchBranchReapState(orch.apiUrl, workspace.repoRoot, workspace.branch, workspace.baseRef, workspace.baseSha)
|
|
426
449
|
: { kind: "unavailable" } as const;
|
|
@@ -520,7 +543,7 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
|
|
|
520
543
|
if (now - firstSeenDeadAt < graceMs) continue;
|
|
521
544
|
|
|
522
545
|
// Grace elapsed + owner dead. Gate on land-state — "nothing would be lost".
|
|
523
|
-
const orch =
|
|
546
|
+
const orch = workspaceOwnerOrchestrator(workspace, orchestrators, { onlineOnly: false, requireApiUrl: true });
|
|
524
547
|
const preview = orch?.apiUrl
|
|
525
548
|
? await fetchWorktreeReapState(orch.apiUrl, workspace.worktreePath, workspace.baseRef)
|
|
526
549
|
: null;
|
package/src/workspace-probe.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
patchWorkspaceMetadata,
|
|
18
18
|
} from "./db";
|
|
19
19
|
import { isPathWithinBase } from "./utils";
|
|
20
|
+
import { workspaceOwnerOrchestrator } from "./workspace-host";
|
|
20
21
|
import { deriveBranchState } from "./workspace-phase";
|
|
21
22
|
import type { AgentCard, WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
22
23
|
import { relayToken } from "./config";
|
|
@@ -95,9 +96,7 @@ export async function probeWorkspaceState(agentId: string): Promise<ProbeResult>
|
|
|
95
96
|
const ws = ownedIsolatedWorkspace(agentId);
|
|
96
97
|
if (!ws || !ws.worktreePath) return { probed: false, skipped: "no live isolated workspace" };
|
|
97
98
|
if (ws.status !== "active") return { probed: false, workspaceId: ws.id, skipped: `status ${ws.status} derives badge without git counts` };
|
|
98
|
-
const orch = listOrchestrators()
|
|
99
|
-
(candidate) => candidate.status === "online" && candidate.apiUrl && workspacePathWithinBase(ws.sourceCwd, candidate.baseDir),
|
|
100
|
-
);
|
|
99
|
+
const orch = workspaceOwnerOrchestrator(ws, listOrchestrators(), { requireApiUrl: true });
|
|
101
100
|
if (!orch?.apiUrl) return { probed: false, workspaceId: ws.id, skipped: "no online orchestrator owns this path" };
|
|
102
101
|
// checkPr:false — a badge probe never needs the gh round-trip (#304).
|
|
103
102
|
const preview = await fetchHostMergePreview(orch.apiUrl, ws, { checkPr: false });
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{Mt as t,Qn as n,Zn as r,er as i,hr as a,mt as o,nr as s,rr as c,wn as l,xn as u}from"./lucide-react-Dc3ZwCBi.js";import{i as d,r as f,t as p}from"./store-OLe8gFh0.js";import{t as m}from"./task-chip-kxk4eNm1.js";import{J as h}from"./index-TamtghSr.js";var g=e(a(),1),_=`(max-width: 767px)`;function v(e){if(typeof window>`u`)return()=>{};let t=window.matchMedia(_);return t.addEventListener(`change`,e),()=>t.removeEventListener(`change`,e)}function y(){return(0,g.useSyncExternalStore)(v,()=>typeof window<`u`?window.matchMedia(_).matches:!1,()=>!1)}var b=s(),x=196,S=84,C=52,w=16,T=14,E=[`landed`,`in-progress`,`pending`,`blocked`],D={landed:{label:`landed`,dot:`bg-emerald-400`,accent:`border-l-emerald-400`,text:`text-emerald-300`,wire:`#34d399`},"in-progress":{label:`in progress`,dot:`bg-sky-400`,accent:`border-l-sky-400`,text:`text-sky-300`,wire:`#38bdf8`},pending:{label:`pending`,dot:`bg-amber-400`,accent:`border-l-amber-400`,text:`text-amber-300`,wire:`#fbbf24`},blocked:{label:`blocked`,dot:`bg-red-400`,accent:`border-l-red-400`,text:`text-red-300`,wire:`#f87171`}};function O(e){return e?e.label||(e.policyName?`policy:${e.policyName}`:e.agentId||``):``}function k(e){let t=new Map(e.nodes.map(e=>[e.id,e])),n=new Map(e.nodes.map((e,t)=>[e.id,t])),r=new Map,i=new Map(e.nodes.map(e=>[e.id,0]));for(let n of e.edges)!t.has(n.from)||!t.has(n.to)||(r.set(n.from,[...r.get(n.from)??[],n.to]),i.set(n.to,(i.get(n.to)??0)+1));let a=new Map(e.nodes.map(e=>[e.id,0])),o=e.nodes.filter(e=>(i.get(e.id)??0)===0).map(e=>e.id),s=new Set;for(;o.length;){let e=o.shift();s.add(e);let t=a.get(e)??0;for(let n of r.get(e)??[])a.set(n,Math.max(a.get(n)??0,t+1)),i.set(n,(i.get(n)??0)-1),(i.get(n)??0)===0&&o.push(n)}let c=Math.max(0,...Array.from(a.values()))+1;for(let t of e.nodes)s.has(t.id)||a.set(t.id,c++);let l=new Map;for(let t of e.nodes){let e=a.get(t.id)??0;l.set(e,[...l.get(e)??[],t])}let u=[...l.entries()].sort(([e],[t])=>e-t),d=Math.max(1,...u.map(([,e])=>e.length)),f=S+w,p=[];u.forEach(([e,t],r)=>{let i=[...t].sort((e,t)=>(e.lane||``).localeCompare(t.lane||``)||(n.get(e.id)??0)-(n.get(t.id)??0)),a=(d-i.length)*f/2;i.forEach((t,n)=>{p.push({node:t,layer:e,x:T+r*(x+C),y:T+a+n*f})})});let m=u.length;return{nodes:p,width:Math.max(320,T*2+m*x+Math.max(0,m-1)*C),height:Math.max(S+T*2,T*2+d*S+Math.max(0,d-1)*w)}}function A(e,t){let n=e.x+x,r=e.y+S/2,i=t.x,a=t.y+S/2,o=n+Math.max(20,(i-n)/2);return`M ${n} ${r} C ${o} ${r}, ${o} ${a}, ${i} ${a}`}function j({threadId:e,teamId:a,channel:o}){let s=a?d(a,o):e?f(e,o):``,c=p(e=>a?e.plansByTeam[s]:s?e.plansByThread[s]:null),_=p(e=>e.fetchPlan),v=p(e=>e.setPlanArchived),C=p(e=>e.planCollapsed),w=p(e=>e.setPlanCollapsed),T=p(e=>e.issueRefsById),j=p(e=>e.deliverableTasks),M=p(e=>e.openTaskDetailById),N=y(),P=C??N;(0,g.useEffect)(()=>{if(a){_({teamId:a,channel:o});return}e&&_({threadId:e,channel:o})},[o,_,a,e]);let F=(0,g.useMemo)(()=>c?k(c):null,[c]),I=(0,g.useMemo)(()=>new Map(F?.nodes.map(e=>[e.node.id,e])??[]),[F]),L=(0,g.useMemo)(()=>{if(!c)return[];let e=new Map;for(let t of c.nodes)e.set(t.status,(e.get(t.status)??0)+1);return E.filter(t=>e.has(t)).map(t=>({status:t,count:e.get(t)??0}))},[c]);if(!c||!F)return null;if(c.archived)return(0,b.jsx)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2`,children:[(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,b.jsx)(r,{className:`h-3 w-3 text-muted-foreground`})}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,b.jsx)(`div`,{className:`truncate text-[13px] font-semibold leading-tight text-muted-foreground`,children:c.title||`Plan`}),(0,b.jsx)(`div`,{className:`truncate text-[11px] leading-tight text-muted-foreground`,children:`Archived plan`})]}),(0,b.jsx)(`span`,{className:`shrink-0 rounded border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground`,children:`archived`}),(0,b.jsx)(`button`,{type:`button`,onClick:()=>void v(c.id,!1),title:`Unarchive plan`,"aria-label":`Unarchive plan`,className:`flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:(0,b.jsx)(n,{className:`h-3.5 w-3.5`})})]})});let R=c.nodes.length,z=L.find(e=>e.status===`landed`)?.count??0,B=`${R} node${R===1?``:`s`}${z>0?` · ${z} landed`:``}`;return(0,b.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,b.jsxs)(`div`,{className:i(`flex min-w-0 items-center gap-2`,!P&&`mb-2`),children:[(0,b.jsxs)(`button`,{type:`button`,onClick:()=>w(!P),"aria-expanded":!P,"aria-label":P?`Expand plan graph`:`Collapse plan graph`,className:`flex min-w-0 flex-1 items-center gap-2 text-left`,children:[(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,b.jsx)(t,{className:`h-3 w-3 text-muted-foreground`})}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,b.jsx)(`div`,{className:`truncate text-[13px] font-semibold leading-tight text-foreground`,children:c.title||`Plan`}),(0,b.jsx)(`div`,{className:`truncate text-[11px] leading-tight text-muted-foreground`,children:P?B:c.objective||B})]})]}),L.length>0&&(0,b.jsx)(`div`,{className:`hidden shrink-0 items-center gap-2.5 sm:flex`,children:L.map(({status:e,count:t})=>(0,b.jsxs)(`span`,{className:`flex items-center gap-1 text-[10px] text-muted-foreground`,children:[(0,b.jsx)(`span`,{className:i(`h-1.5 w-1.5 rounded-full`,D[e].dot)}),(0,b.jsx)(`span`,{className:i(`font-medium tabular-nums`,D[e].text),children:t}),(0,b.jsx)(`span`,{children:D[e].label})]},e))}),(0,b.jsxs)(`span`,{className:`shrink-0 rounded bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground`,children:[`v`,c.version]}),(0,b.jsx)(`button`,{type:`button`,onClick:()=>void v(c.id,!0),title:`Archive plan`,"aria-label":`Archive plan`,className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:(0,b.jsx)(r,{className:`h-3.5 w-3.5`})}),(0,b.jsx)(`button`,{type:`button`,onClick:()=>w(!P),"aria-expanded":!P,"aria-label":P?`Expand plan graph`:`Collapse plan graph`,className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:P?(0,b.jsx)(l,{className:`h-3.5 w-3.5`}):(0,b.jsx)(u,{className:`h-3.5 w-3.5`})})]}),P?null:c.nodes.length===0?(0,b.jsx)(`div`,{className:`rounded-md border border-dashed border-border px-3 py-3 text-center text-xs text-muted-foreground`,children:`No nodes yet — the plan graph fills in as the team is composed.`}):(0,b.jsx)(`div`,{className:`max-h-[min(46vh,360px)] overflow-auto`,children:(0,b.jsxs)(`div`,{className:`relative`,style:{width:F.width,height:F.height},children:[(0,b.jsxs)(`svg`,{className:`pointer-events-none absolute inset-0`,width:F.width,height:F.height,"aria-hidden":`true`,children:[(0,b.jsx)(`defs`,{children:E.map(e=>(0,b.jsx)(`marker`,{id:`plan-arrow-${e}`,viewBox:`0 0 8 8`,refX:`6.5`,refY:`4`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`,children:(0,b.jsx)(`path`,{d:`M0 0 L8 4 L0 8 z`,fill:D[e].wire})},e))}),c.edges.map(e=>{let t=I.get(e.from),n=I.get(e.to);if(!t||!n)return null;let r=D[t.node.status];return(0,b.jsx)(`path`,{d:A(t,n),stroke:r.wire,strokeOpacity:.7,strokeWidth:2,fill:`none`,strokeLinecap:`round`,markerEnd:`url(#plan-arrow-${t.node.status})`},`${e.from}->${e.to}`)})]}),F.nodes.map(({node:e,x:t,y:n})=>{let r=D[e.status],a=O(e.ownerRef),o=e.refs?.issueId?T[e.refs.issueId]:null,s=e.refs?.taskId?parseInt(e.refs.taskId,10):null,c=s==null?void 0:j.find(e=>e.id===s)?.title;return(0,b.jsxs)(`article`,{className:i(`group absolute flex flex-col justify-between rounded-md border border-l-[3px] bg-background/95 p-2`,`shadow-sm transition-all hover:-translate-y-px hover:border-border hover:shadow-md hover:bg-background`,r.accent),style:{left:t,top:n,width:x,height:S},title:e.scopeBlurb||e.label,children:[(0,b.jsx)(`div`,{className:`truncate text-xs font-semibold leading-tight text-foreground`,children:e.label}),(e.scopeBlurb||e.id)&&(0,b.jsx)(`div`,{className:`truncate text-[10px] leading-tight text-muted-foreground`,children:e.scopeBlurb||e.id}),(o||s!=null)&&(0,b.jsxs)(`div`,{className:`flex min-w-0 flex-wrap gap-1`,children:[o&&(0,b.jsx)(h,{issueRef:o,compact:!0,className:`max-w-full`}),s!=null&&(0,b.jsx)(m,{id:s,title:c,onClick:()=>M(s),className:`max-w-full`})]}),(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center justify-between gap-1.5`,children:[(0,b.jsxs)(`div`,{className:`flex shrink-0 items-center gap-1.5`,children:[(0,b.jsx)(`span`,{className:i(`h-1.5 w-1.5 shrink-0 rounded-full`,r.dot)}),(0,b.jsx)(`span`,{className:i(`text-[10px] font-medium`,r.text),children:r.label})]}),(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center gap-1.5`,children:[a&&(0,b.jsx)(`span`,{className:`truncate text-[10px] text-muted-foreground`,children:a}),e.lane&&(0,b.jsx)(`span`,{className:`shrink-0 rounded-sm bg-muted/70 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground`,children:e.lane})]})]})]},e.id)})]})})]})}function M({agentId:e}){let t=p(e=>e.openTaskDetailById),n=p(e=>e.sessionTasksCollapsed),r=p(e=>e.setSessionTasksCollapsed),[a,s]=(0,g.useState)([]);return(0,g.useEffect)(()=>{c(`GET`,`/task-entities?createdBy=`+encodeURIComponent(e)).then(e=>s(e)).catch(()=>{})},[e]),a.length===0?null:(0,b.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,b.jsxs)(`button`,{type:`button`,onClick:()=>r(!n),"aria-expanded":!n,"aria-label":n?`Expand session tasks`:`Collapse session tasks`,className:i(`flex w-full min-w-0 items-center gap-2 text-left`,!n&&`mb-1.5`),children:[(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,b.jsx)(o,{className:`h-3 w-3 text-muted-foreground`})}),(0,b.jsxs)(`span`,{className:`flex-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground`,children:[`Tasks from this session (`,a.length,`)`]}),(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:n?(0,b.jsx)(l,{className:`h-3.5 w-3.5`}):(0,b.jsx)(u,{className:`h-3.5 w-3.5`})})]}),!n&&(0,b.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:a.map(e=>(0,b.jsx)(m,{id:e.id,title:e.title,status:e.status,onClick:()=>t(e.id),compact:!0},e.id))})]})}export{M as n,j as t};
|
|
2
|
-
//# sourceMappingURL=PlanGraphPanel-B9TN_KQr.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PlanGraphPanel-B9TN_KQr.js","names":[],"sources":["../../dashboard/src/components/chat/hooks/useIsMobile.ts","../../dashboard/src/components/chat/PlanGraphPanel.tsx"],"sourcesContent":["import { useSyncExternalStore } from 'react'\n\n// Reactive narrow-viewport flag. Subscribes to the same `max-width: 767px`\n// breakpoint the chat single-pane layout uses, via useSyncExternalStore — an\n// external-store subscription, not effect-driven derived state — so it stays\n// correct across resize/rotate without a useEffect.\nconst QUERY = '(max-width: 767px)'\n\nfunction subscribe(cb: () => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const mql = window.matchMedia(QUERY)\n mql.addEventListener('change', cb)\n return () => mql.removeEventListener('change', cb)\n}\n\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(\n subscribe,\n () => (typeof window !== 'undefined' ? window.matchMedia(QUERY).matches : false),\n () => false,\n )\n}\n","import { useEffect, useMemo, useState } from 'react'\nimport { Archive, ArchiveRestore, ChevronDown, ChevronUp, GitBranch, ListTodo } from 'lucide-react'\nimport { useRelayStore } from '@/store'\nimport type { Plan, PlanNode, PlanOwnerRef, PlanStatus, Task } from '@/types'\nimport { cn } from '@/lib/utils'\nimport { planLookupKey, planTeamLookupKey } from '@/store/slices/plans'\nimport { IssueRefChip } from '@/components/shared/issue-ref-chip'\nimport { TaskChip } from '@/components/shared/task-chip'\nimport { useIsMobile } from './hooks/useIsMobile'\nimport { api } from '@/lib/api'\n\n// Left-to-right DAG: dependency depth flows along x (sequential gates), nodes\n// that share a depth stack along y (parallel lanes). This keeps the panel SHORT\n// — it's an operator overlay above the chat, not a full view — and lets the\n// gate-vs-lane structure read at a glance. Height is capped + scrollable below.\nconst NODE_W = 196\nconst NODE_H = 84\nconst COL_GAP = 52\nconst ROW_GAP = 16\nconst PAD = 14\n\nconst STATUS_ORDER: PlanStatus[] = ['landed', 'in-progress', 'pending', 'blocked']\n\nconst STATUS_STYLE: Record<\n PlanStatus,\n { label: string; dot: string; accent: string; text: string; wire: string }\n> = {\n landed: { label: 'landed', dot: 'bg-emerald-400', accent: 'border-l-emerald-400', text: 'text-emerald-300', wire: '#34d399' },\n 'in-progress': { label: 'in progress', dot: 'bg-sky-400', accent: 'border-l-sky-400', text: 'text-sky-300', wire: '#38bdf8' },\n pending: { label: 'pending', dot: 'bg-amber-400', accent: 'border-l-amber-400', text: 'text-amber-300', wire: '#fbbf24' },\n blocked: { label: 'blocked', dot: 'bg-red-400', accent: 'border-l-red-400', text: 'text-red-300', wire: '#f87171' },\n}\n\ninterface LayoutNode {\n node: PlanNode\n x: number\n y: number\n layer: number\n}\n\nfunction ownerLabel(owner?: PlanOwnerRef): string {\n if (!owner) return ''\n return owner.label || (owner.policyName ? `policy:${owner.policyName}` : owner.agentId || '')\n}\n\nfunction layerPlan(plan: Plan): { nodes: LayoutNode[]; width: number; height: number } {\n const nodeById = new Map(plan.nodes.map((node) => [node.id, node]))\n const indexById = new Map(plan.nodes.map((node, index) => [node.id, index]))\n const outgoing = new Map<string, string[]>()\n const indegree = new Map(plan.nodes.map((node) => [node.id, 0]))\n for (const edge of plan.edges) {\n if (!nodeById.has(edge.from) || !nodeById.has(edge.to)) continue\n outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to])\n indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1)\n }\n\n const layerById = new Map(plan.nodes.map((node) => [node.id, 0]))\n const queue = plan.nodes\n .filter((node) => (indegree.get(node.id) ?? 0) === 0)\n .map((node) => node.id)\n const visited = new Set<string>()\n\n while (queue.length) {\n const id = queue.shift()!\n visited.add(id)\n const fromLayer = layerById.get(id) ?? 0\n for (const to of outgoing.get(id) ?? []) {\n layerById.set(to, Math.max(layerById.get(to) ?? 0, fromLayer + 1))\n indegree.set(to, (indegree.get(to) ?? 0) - 1)\n if ((indegree.get(to) ?? 0) === 0) queue.push(to)\n }\n }\n\n let fallbackLayer = Math.max(0, ...Array.from(layerById.values())) + 1\n for (const node of plan.nodes) {\n if (visited.has(node.id)) continue\n layerById.set(node.id, fallbackLayer++)\n }\n\n const layers = new Map<number, PlanNode[]>()\n for (const node of plan.nodes) {\n const layer = layerById.get(node.id) ?? 0\n layers.set(layer, [...(layers.get(layer) ?? []), node])\n }\n\n const orderedLayers = [...layers.entries()].sort(([a], [b]) => a - b)\n const maxRows = Math.max(1, ...orderedLayers.map(([, nodes]) => nodes.length))\n const rowPitch = NODE_H + ROW_GAP\n\n const layoutNodes: LayoutNode[] = []\n orderedLayers.forEach(([layer, nodes], col) => {\n const ordered = [...nodes].sort((a, b) => {\n const lane = (a.lane || '').localeCompare(b.lane || '')\n return lane || ((indexById.get(a.id) ?? 0) - (indexById.get(b.id) ?? 0))\n })\n // Center each column vertically against the tallest column so the spine\n // runs through the middle and the parallel fan reads as a fan.\n const offset = ((maxRows - ordered.length) * rowPitch) / 2\n ordered.forEach((node, row) => {\n layoutNodes.push({\n node,\n layer,\n x: PAD + col * (NODE_W + COL_GAP),\n y: PAD + offset + row * rowPitch,\n })\n })\n })\n\n const cols = orderedLayers.length\n return {\n nodes: layoutNodes,\n width: Math.max(320, PAD * 2 + cols * NODE_W + Math.max(0, cols - 1) * COL_GAP),\n height: Math.max(NODE_H + PAD * 2, PAD * 2 + maxRows * NODE_H + Math.max(0, maxRows - 1) * ROW_GAP),\n }\n}\n\nfunction edgePath(from: LayoutNode, to: LayoutNode): string {\n const x1 = from.x + NODE_W\n const y1 = from.y + NODE_H / 2\n const x2 = to.x\n const y2 = to.y + NODE_H / 2\n const midX = x1 + Math.max(20, (x2 - x1) / 2)\n return `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`\n}\n\nexport function PlanGraphPanel({ threadId, teamId, channel }: { threadId?: string | number | null; teamId?: string | null; channel?: string }) {\n const key = teamId ? planTeamLookupKey(teamId, channel) : threadId ? planLookupKey(threadId, channel) : ''\n const plan = useRelayStore((s) => teamId ? s.plansByTeam[key] : key ? s.plansByThread[key] : null)\n const fetchPlan = useRelayStore((s) => s.fetchPlan)\n const setPlanArchived = useRelayStore((s) => s.setPlanArchived)\n const planCollapsed = useRelayStore((s) => s.planCollapsed)\n const setPlanCollapsed = useRelayStore((s) => s.setPlanCollapsed)\n const issueRefsById = useRelayStore((s) => s.issueRefsById)\n const deliverableTasks = useRelayStore((s) => s.deliverableTasks)\n const openTaskDetailById = useRelayStore((s) => s.openTaskDetailById)\n const isMobile = useIsMobile()\n // null = no manual toggle yet → follow the viewport default. The graph\n // dominates a phone screen, so it starts collapsed there and expanded on\n // desktop; an explicit user toggle (persisted) overrides the default.\n const collapsed = planCollapsed ?? isMobile\n\n useEffect(() => {\n if (teamId) {\n void fetchPlan({ teamId, channel })\n return\n }\n if (threadId) void fetchPlan({ threadId, channel })\n }, [channel, fetchPlan, teamId, threadId])\n\n const layout = useMemo(() => plan ? layerPlan(plan) : null, [plan])\n const nodeById = useMemo(() => new Map(layout?.nodes.map((node) => [node.node.id, node]) ?? []), [layout])\n\n const legend = useMemo(() => {\n if (!plan) return []\n const counts = new Map<PlanStatus, number>()\n for (const node of plan.nodes) counts.set(node.status, (counts.get(node.status) ?? 0) + 1)\n return STATUS_ORDER.filter((status) => counts.has(status)).map((status) => ({\n status,\n count: counts.get(status) ?? 0,\n }))\n }, [plan])\n\n if (!plan || !layout) return null\n\n if (plan.archived) {\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <Archive className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[13px] font-semibold leading-tight text-muted-foreground\">{plan.title || 'Plan'}</div>\n <div className=\"truncate text-[11px] leading-tight text-muted-foreground\">Archived plan</div>\n </div>\n <span className=\"shrink-0 rounded border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n archived\n </span>\n <button\n type=\"button\"\n onClick={() => void setPlanArchived(plan.id, false)}\n title=\"Unarchive plan\"\n aria-label=\"Unarchive plan\"\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n >\n <ArchiveRestore className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </section>\n )\n }\n\n const nodeCount = plan.nodes.length\n const landedCount = legend.find((entry) => entry.status === 'landed')?.count ?? 0\n // Thin summary for the collapsed bar (the legend dots are hidden on mobile),\n // e.g. \"5 nodes · 4 landed\".\n const summary = `${nodeCount} node${nodeCount === 1 ? '' : 's'}${landedCount > 0 ? ` · ${landedCount} landed` : ''}`\n\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <div className={cn('flex min-w-0 items-center gap-2', !collapsed && 'mb-2')}>\n <button\n type=\"button\"\n onClick={() => setPlanCollapsed(!collapsed)}\n aria-expanded={!collapsed}\n aria-label={collapsed ? 'Expand plan graph' : 'Collapse plan graph'}\n className=\"flex min-w-0 flex-1 items-center gap-2 text-left\"\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <GitBranch className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[13px] font-semibold leading-tight text-foreground\">{plan.title || 'Plan'}</div>\n <div className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {collapsed ? summary : (plan.objective || summary)}\n </div>\n </div>\n </button>\n {legend.length > 0 && (\n <div className=\"hidden shrink-0 items-center gap-2.5 sm:flex\">\n {legend.map(({ status, count }) => (\n <span key={status} className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n <span className={cn('h-1.5 w-1.5 rounded-full', STATUS_STYLE[status].dot)} />\n <span className={cn('font-medium tabular-nums', STATUS_STYLE[status].text)}>{count}</span>\n <span>{STATUS_STYLE[status].label}</span>\n </span>\n ))}\n </div>\n )}\n <span className=\"shrink-0 rounded bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground\">\n v{plan.version}\n </span>\n <button\n type=\"button\"\n onClick={() => void setPlanArchived(plan.id, true)}\n title=\"Archive plan\"\n aria-label=\"Archive plan\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n >\n <Archive className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setPlanCollapsed(!collapsed)}\n aria-expanded={!collapsed}\n aria-label={collapsed ? 'Expand plan graph' : 'Collapse plan graph'}\n className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n >\n {collapsed ? <ChevronDown className=\"h-3.5 w-3.5\" /> : <ChevronUp className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n {collapsed ? null : plan.nodes.length === 0 ? (\n <div className=\"rounded-md border border-dashed border-border px-3 py-3 text-center text-xs text-muted-foreground\">\n No nodes yet — the plan graph fills in as the team is composed.\n </div>\n ) : (\n <div className=\"max-h-[min(46vh,360px)] overflow-auto\">\n <div className=\"relative\" style={{ width: layout.width, height: layout.height }}>\n <svg className=\"pointer-events-none absolute inset-0\" width={layout.width} height={layout.height} aria-hidden=\"true\">\n <defs>\n {STATUS_ORDER.map((status) => (\n <marker\n key={status}\n id={`plan-arrow-${status}`}\n viewBox=\"0 0 8 8\"\n refX=\"6.5\"\n refY=\"4\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M0 0 L8 4 L0 8 z\" fill={STATUS_STYLE[status].wire} />\n </marker>\n ))}\n </defs>\n {plan.edges.map((edge) => {\n const from = nodeById.get(edge.from)\n const to = nodeById.get(edge.to)\n if (!from || !to) return null\n const wire = STATUS_STYLE[from.node.status]\n return (\n <path\n key={`${edge.from}->${edge.to}`}\n d={edgePath(from, to)}\n stroke={wire.wire}\n strokeOpacity={0.7}\n strokeWidth={2}\n fill=\"none\"\n strokeLinecap=\"round\"\n markerEnd={`url(#plan-arrow-${from.node.status})`}\n />\n )\n })}\n </svg>\n {layout.nodes.map(({ node, x, y }) => {\n const status = STATUS_STYLE[node.status]\n const owner = ownerLabel(node.ownerRef)\n const issueRef = node.refs?.issueId ? issueRefsById[node.refs.issueId] : null\n const taskNumId = node.refs?.taskId ? parseInt(node.refs.taskId, 10) : null\n const taskTitle = taskNumId != null ? deliverableTasks.find((t) => t.id === taskNumId)?.title : undefined\n return (\n <article\n key={node.id}\n className={cn(\n 'group absolute flex flex-col justify-between rounded-md border border-l-[3px] bg-background/95 p-2',\n 'shadow-sm transition-all hover:-translate-y-px hover:border-border hover:shadow-md hover:bg-background',\n status.accent,\n )}\n style={{ left: x, top: y, width: NODE_W, height: NODE_H }}\n title={node.scopeBlurb || node.label}\n >\n <div className=\"truncate text-xs font-semibold leading-tight text-foreground\">{node.label}</div>\n {(node.scopeBlurb || node.id) && (\n <div className=\"truncate text-[10px] leading-tight text-muted-foreground\">{node.scopeBlurb || node.id}</div>\n )}\n {(issueRef || taskNumId != null) && (\n <div className=\"flex min-w-0 flex-wrap gap-1\">\n {issueRef && <IssueRefChip issueRef={issueRef} compact className=\"max-w-full\" />}\n {taskNumId != null && (\n <TaskChip id={taskNumId} title={taskTitle} onClick={() => openTaskDetailById(taskNumId)} className=\"max-w-full\" />\n )}\n </div>\n )}\n <div className=\"flex min-w-0 items-center justify-between gap-1.5\">\n <div className=\"flex shrink-0 items-center gap-1.5\">\n <span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', status.dot)} />\n <span className={cn('text-[10px] font-medium', status.text)}>{status.label}</span>\n </div>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n {owner && <span className=\"truncate text-[10px] text-muted-foreground\">{owner}</span>}\n {node.lane && (\n <span className=\"shrink-0 rounded-sm bg-muted/70 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground\">\n {node.lane}\n </span>\n )}\n </div>\n </div>\n </article>\n )\n })}\n </div>\n </div>\n )}\n </section>\n )\n}\n\n// Tasks created/triggered from a session (#856, #860). Fetches server-side via createdBy filter.\nexport function SessionTasksPanel({ agentId }: { agentId: string }) {\n const openTaskDetailById = useRelayStore((s) => s.openTaskDetailById)\n const sessionTasksCollapsed = useRelayStore((s) => s.sessionTasksCollapsed)\n const setSessionTasksCollapsed = useRelayStore((s) => s.setSessionTasksCollapsed)\n const [sessionTasks, setSessionTasks] = useState<Task[]>([])\n useEffect(() => {\n void api<import('@/types').Task[]>('GET', '/task-entities?createdBy=' + encodeURIComponent(agentId))\n .then((tasks) => setSessionTasks(tasks))\n .catch(() => {})\n }, [agentId])\n if (sessionTasks.length === 0) return null\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <button\n type=\"button\"\n onClick={() => setSessionTasksCollapsed(!sessionTasksCollapsed)}\n aria-expanded={!sessionTasksCollapsed}\n aria-label={sessionTasksCollapsed ? 'Expand session tasks' : 'Collapse session tasks'}\n className={cn('flex w-full min-w-0 items-center gap-2 text-left', !sessionTasksCollapsed && 'mb-1.5')}\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <ListTodo className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <span className=\"flex-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n Tasks from this session ({sessionTasks.length})\n </span>\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\">\n {sessionTasksCollapsed ? <ChevronDown className=\"h-3.5 w-3.5\" /> : <ChevronUp className=\"h-3.5 w-3.5\" />}\n </span>\n </button>\n {!sessionTasksCollapsed && (\n <div className=\"flex flex-wrap gap-1\">\n {sessionTasks.map((task) => (\n <TaskChip\n key={task.id}\n id={task.id}\n title={task.title}\n status={task.status}\n onClick={() => openTaskDetailById(task.id)}\n compact\n />\n ))}\n </div>\n )}\n </section>\n )\n}\n"],"mappings":"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"}
|