agent-relay-server 0.61.2 → 0.62.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/docs/openapi.json +479 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-DVvlvi4B.js → MessageBubble-BP0Ym3xb.js} +2 -2
- package/public/assets/{MessageBubble-DVvlvi4B.js.map → MessageBubble-BP0Ym3xb.js.map} +1 -1
- package/public/assets/PlanGraphPanel-DKc3gdou.js +2 -0
- package/public/assets/PlanGraphPanel-DKc3gdou.js.map +1 -0
- package/public/assets/{activity-IYl3MEXC.js → activity-CRLG6ICe.js} +2 -2
- package/public/assets/{activity-IYl3MEXC.js.map → activity-CRLG6ICe.js.map} +1 -1
- package/public/assets/{agent-profiles-BfGTTbeY.js → agent-profiles-DItyEkXP.js} +2 -2
- package/public/assets/{agent-profiles-BfGTTbeY.js.map → agent-profiles-DItyEkXP.js.map} +1 -1
- package/public/assets/{agents-BquvREQr.js → agents-5mKiZ1rz.js} +2 -2
- package/public/assets/{agents-BquvREQr.js.map → agents-5mKiZ1rz.js.map} +1 -1
- package/public/assets/{analytics-De0cEZGx.js → analytics-J5pPcS-v.js} +3 -3
- package/public/assets/{analytics-De0cEZGx.js.map → analytics-J5pPcS-v.js.map} +1 -1
- package/public/assets/{automation-Ddbermbi.js → automation-BG2HWK5N.js} +2 -2
- package/public/assets/{automation-Ddbermbi.js.map → automation-BG2HWK5N.js.map} +1 -1
- package/public/assets/{badge-DhLyCPag.js → badge-CZhIdlTJ.js} +2 -2
- package/public/assets/{badge-DhLyCPag.js.map → badge-CZhIdlTJ.js.map} +1 -1
- package/public/assets/{branch-state-badge-DSyWKluD.js → branch-state-badge-BZMoxs65.js} +2 -2
- package/public/assets/{branch-state-badge-DSyWKluD.js.map → branch-state-badge-BZMoxs65.js.map} +1 -1
- package/public/assets/{button-K20wAZG1.js → button-2taQeNHW.js} +2 -2
- package/public/assets/{button-K20wAZG1.js.map → button-2taQeNHW.js.map} +1 -1
- package/public/assets/card-DzIsAI41.js +2 -0
- package/public/assets/{card-CcZ9hLI7.js.map → card-DzIsAI41.js.map} +1 -1
- package/public/assets/channels-B2PeqiRT.js +2 -0
- package/public/assets/{channels-BG0Nib7y.js.map → channels-B2PeqiRT.js.map} +1 -1
- package/public/assets/chat-CLRJVagA.js +2 -0
- package/public/assets/{chat-Dx6qWNxb.js.map → chat-CLRJVagA.js.map} +1 -1
- package/public/assets/{connectors-DWh7MwV_.js → connectors-Bzvkxsgo.js} +2 -2
- package/public/assets/{connectors-DWh7MwV_.js.map → connectors-Bzvkxsgo.js.map} +1 -1
- package/public/assets/copy-button-HEkwi6xX.js +2 -0
- package/public/assets/{copy-button-eF3cKp4Q.js.map → copy-button-HEkwi6xX.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js.map +1 -1
- package/public/assets/dist-1rXN5FN9.js +2 -0
- package/public/assets/{dist-YpHfnnIA.js.map → dist-1rXN5FN9.js.map} +1 -1
- package/public/assets/{dist-1dhfdhm9.js → dist-AbEQuFoK.js} +2 -2
- package/public/assets/{dist-1dhfdhm9.js.map → dist-AbEQuFoK.js.map} +1 -1
- package/public/assets/{dist-COQuunEv.js → dist-B7p1zkvf.js} +2 -2
- package/public/assets/{dist-COQuunEv.js.map → dist-B7p1zkvf.js.map} +1 -1
- package/public/assets/{dist-Ci5Y37nj.js → dist-CG10hM1-.js} +2 -2
- package/public/assets/{dist-Ci5Y37nj.js.map → dist-CG10hM1-.js.map} +1 -1
- package/public/assets/dist-Cfm5_zPB.js +2 -0
- package/public/assets/{dist-qd198ib9.js.map → dist-Cfm5_zPB.js.map} +1 -1
- package/public/assets/{dist-BrQsqBbc.js → dist-Cgw85UcE.js} +2 -2
- package/public/assets/{dist-BrQsqBbc.js.map → dist-Cgw85UcE.js.map} +1 -1
- package/public/assets/{dist-CoROafNg.js → dist-D3_TJKTC.js} +2 -2
- package/public/assets/{dist-CoROafNg.js.map → dist-D3_TJKTC.js.map} +1 -1
- package/public/assets/{dist-I-MFUv51.js → dist-DQkLq0hs.js} +2 -2
- package/public/assets/{dist-I-MFUv51.js.map → dist-DQkLq0hs.js.map} +1 -1
- package/public/assets/{dist-UQGKwqZH.js → dist-D_sl7u_f.js} +2 -2
- package/public/assets/{dist-UQGKwqZH.js.map → dist-D_sl7u_f.js.map} +1 -1
- package/public/assets/{dist-yGOq6sB3.js → dist-Ilb3r1im.js} +2 -2
- package/public/assets/{dist-yGOq6sB3.js.map → dist-Ilb3r1im.js.map} +1 -1
- package/public/assets/dist-Pf_3Gf38.js +2 -0
- package/public/assets/{dist-BLBF7qqr.js.map → dist-Pf_3Gf38.js.map} +1 -1
- package/public/assets/{es2015-CRxz9lOj.js → es2015-GSepNaIS.js} +2 -2
- package/public/assets/{es2015-CRxz9lOj.js.map → es2015-GSepNaIS.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DMm6jeqi.js → formatted-body-impl-QqHxdMGP.js} +2 -2
- package/public/assets/{formatted-body-impl-DMm6jeqi.js.map → formatted-body-impl-QqHxdMGP.js.map} +1 -1
- package/public/assets/index-CCp1V35c.js +22 -0
- package/public/assets/index-CCp1V35c.js.map +1 -0
- package/public/assets/index-DiR-fqy_.css +2 -0
- package/public/assets/{input-BHYNpXAG.js → input-BsNqlONJ.js} +2 -2
- package/public/assets/{input-BHYNpXAG.js.map → input-BsNqlONJ.js.map} +1 -1
- package/public/assets/{insights-C7wKt1_l.js → insights-D-EyDfoZ.js} +2 -2
- package/public/assets/{insights-C7wKt1_l.js.map → insights-D-EyDfoZ.js.map} +1 -1
- package/public/assets/{integrations-CG9qyHUf.js → integrations-D1kc-yNi.js} +2 -2
- package/public/assets/{integrations-CG9qyHUf.js.map → integrations-D1kc-yNi.js.map} +1 -1
- package/public/assets/{lib-Df4aph5u.js → lib-bXPu6xCL.js} +2 -2
- package/public/assets/{lib-Df4aph5u.js.map → lib-bXPu6xCL.js.map} +1 -1
- package/public/assets/lucide-react-DWYjU-AF.js +9 -0
- package/public/assets/{lucide-react-L2UxFDic.js.map → lucide-react-DWYjU-AF.js.map} +1 -1
- package/public/assets/{maintenance-CjpvnV_U.js → maintenance-COqID7aq.js} +2 -2
- package/public/assets/{maintenance-CjpvnV_U.js.map → maintenance-COqID7aq.js.map} +1 -1
- package/public/assets/{managed-agents-CdfuBKJA.js → managed-agents-b4B8qewQ.js} +2 -2
- package/public/assets/{managed-agents-CdfuBKJA.js.map → managed-agents-b4B8qewQ.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-Cf4xCmWt.js → markdown-preview-impl-D7o8jS07.js} +2 -2
- package/public/assets/{markdown-preview-impl-Cf4xCmWt.js.map → markdown-preview-impl-D7o8jS07.js.map} +1 -1
- package/public/assets/memory-Q0qF7_i3.js +2 -0
- package/public/assets/{memory-xKWduhyr.js.map → memory-Q0qF7_i3.js.map} +1 -1
- package/public/assets/{messages-BpZYOUtJ.js → messages-m4EpkK6s.js} +2 -2
- package/public/assets/{messages-BpZYOUtJ.js.map → messages-m4EpkK6s.js.map} +1 -1
- package/public/assets/{orchestrators-BrJvGIOc.js → orchestrators-CnAtGfUN.js} +2 -2
- package/public/assets/{orchestrators-BrJvGIOc.js.map → orchestrators-CnAtGfUN.js.map} +1 -1
- package/public/assets/{overview-BUORwePi.js → overview-D7CF88ik.js} +2 -2
- package/public/assets/{overview-BUORwePi.js.map → overview-D7CF88ik.js.map} +1 -1
- package/public/assets/pairs-Q6PlFJtH.js +2 -0
- package/public/assets/{pairs-DR0GizDN.js.map → pairs-Q6PlFJtH.js.map} +1 -1
- package/public/assets/{react-dom-CrP_AtcL.js → react-dom-CPghDLpB.js} +2 -2
- package/public/assets/{react-dom-CrP_AtcL.js.map → react-dom-CPghDLpB.js.map} +1 -1
- package/public/assets/{security-DLwSqTc_.js → security-BeV-6SdA.js} +2 -2
- package/public/assets/{security-DLwSqTc_.js.map → security-BeV-6SdA.js.map} +1 -1
- package/public/assets/{select-pEru111r.js → select-Dchc0_Jg.js} +2 -2
- package/public/assets/{select-pEru111r.js.map → select-Dchc0_Jg.js.map} +1 -1
- package/public/assets/{settings-B-ueGvwV.js → settings-hPwL-31u.js} +3 -3
- package/public/assets/{settings-B-ueGvwV.js.map → settings-hPwL-31u.js.map} +1 -1
- package/public/assets/store-Buwkqv_7.js +9 -0
- package/public/assets/store-Buwkqv_7.js.map +1 -0
- package/public/assets/tasks-DKUMPEgk.js +2 -0
- package/public/assets/{tasks-C2rRQm6E.js.map → tasks-DKUMPEgk.js.map} +1 -1
- package/public/assets/teams-T5fFsdOX.js +2 -0
- package/public/assets/teams-T5fFsdOX.js.map +1 -0
- package/public/assets/{terminal-viewer-impl-BM_3qoeN.js → terminal-viewer-impl-Bk0u_Mav.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BM_3qoeN.js.map → terminal-viewer-impl-Bk0u_Mav.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js → use-keyboard-viewport-NKw1O5gC.js} +2 -2
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js.map → use-keyboard-viewport-NKw1O5gC.js.map} +1 -1
- package/public/assets/{work-queue-CkIYDAZD.js → work-queue-D26jok09.js} +2 -2
- package/public/assets/{work-queue-CkIYDAZD.js.map → work-queue-D26jok09.js.map} +1 -1
- package/public/assets/{workspaces-CYm5bLg9.js → workspaces-BMj6zx23.js} +3 -3
- package/public/assets/{workspaces-CYm5bLg9.js.map → workspaces-BMj6zx23.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/automations.ts +40 -7
- package/src/branch-landed.ts +5 -0
- package/src/bus.ts +3 -0
- package/src/cli/workspace.ts +9 -17
- package/src/db/activity.ts +191 -3
- package/src/db/integrations.ts +1 -0
- package/src/db/issue-associations.ts +128 -0
- package/src/db/issue-lifecycle.ts +187 -0
- package/src/db/issue-refs.ts +254 -0
- package/src/db/mappers.ts +2 -0
- package/src/db/migrations.ts +47 -0
- package/src/db/schema.ts +2 -2
- package/src/issue-tracker/adapter.ts +24 -0
- package/src/issue-tracker/github.ts +136 -0
- package/src/issue-tracker/index.ts +13 -0
- package/src/issue-tracker/registry.ts +26 -0
- package/src/maintenance.ts +22 -169
- package/src/mcp-issue-activity-tools.ts +114 -0
- package/src/mcp-issue-association-tools.ts +130 -0
- package/src/mcp-issue-lifecycle-tools.ts +55 -0
- package/src/mcp-issue-ref-tools.ts +174 -0
- package/src/mcp-team-tools.ts +1 -1
- package/src/mcp.ts +3 -3
- package/src/provider-config-store.ts +30 -8
- package/src/routes/activity.ts +6 -1
- package/src/routes/commands.ts +29 -0
- package/src/routes/index.ts +19 -0
- package/src/routes/issue-activity.ts +78 -0
- package/src/routes/issue-associations.ts +76 -0
- package/src/routes/issue-lifecycle.ts +34 -0
- package/src/routes/issue-refs.ts +137 -0
- package/src/routes/orchestrator.ts +42 -1
- package/src/security.ts +8 -0
- package/src/services/issue-activity.ts +30 -0
- package/src/services/issue-associations.ts +72 -0
- package/src/services/issue-lifecycle.ts +308 -0
- package/src/services/issue-refs.ts +32 -0
- package/src/services/plan-graph.ts +40 -2
- package/src/services/team.ts +4 -0
- package/src/token-db.ts +3 -3
- package/src/workspace-actions.ts +8 -1
- package/src/workspace-auto-merge.ts +228 -0
- package/src/workspace-probe.ts +125 -0
- package/public/assets/PlanGraphPanel-Dby7wq9c.js +0 -2
- package/public/assets/PlanGraphPanel-Dby7wq9c.js.map +0 -1
- package/public/assets/card-CcZ9hLI7.js +0 -2
- package/public/assets/channels-BG0Nib7y.js +0 -2
- package/public/assets/chat-Dx6qWNxb.js +0 -2
- package/public/assets/copy-button-eF3cKp4Q.js +0 -2
- package/public/assets/dist-BLBF7qqr.js +0 -2
- package/public/assets/dist-YpHfnnIA.js +0 -2
- package/public/assets/dist-qd198ib9.js +0 -2
- package/public/assets/index-B3wOeAPO.js +0 -21
- package/public/assets/index-B3wOeAPO.js.map +0 -1
- package/public/assets/index-Dn_GDXbi.css +0 -2
- package/public/assets/lucide-react-L2UxFDic.js +0 -9
- package/public/assets/memory-xKWduhyr.js +0 -2
- package/public/assets/pairs-DR0GizDN.js +0 -2
- package/public/assets/store-B8xP6z6T.js +0 -9
- package/public/assets/store-B8xP6z6T.js.map +0 -1
- package/public/assets/tasks-C2rRQm6E.js +0 -2
- package/public/assets/teams-Bo0sdBTp.js +0 -2
- package/public/assets/teams-Bo0sdBTp.js.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
2
|
import { createPlanRow, deletePlan, getAgent, getEntry, getPlan, getPlanByTeam, getPlanByThread, listPlansWithBoundOwner, planThreadExists, replacePlan, ValidationError } from "../db";
|
|
3
|
+
import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/issue-associations";
|
|
4
|
+
import { resolveIssueRefFromString } from "../db/issue-refs";
|
|
3
5
|
import { emitRelayEvent } from "../events";
|
|
4
6
|
import { cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
5
7
|
import type { AuthContext } from "./auth-context";
|
|
@@ -72,7 +74,37 @@ function cleanOwnerRef(value: unknown): PlanOwnerRef | undefined {
|
|
|
72
74
|
|
|
73
75
|
function cleanRefs(value: unknown): PlanNode["refs"] | undefined {
|
|
74
76
|
if (value === undefined || value === null) return undefined;
|
|
75
|
-
|
|
77
|
+
const ref = cleanRecordRef(value, "refs");
|
|
78
|
+
if (!ref) return undefined;
|
|
79
|
+
const out: PlanNode["refs"] = {};
|
|
80
|
+
const issueId = cleanString(ref.issueId, "refs.issueId", { max: 1000 });
|
|
81
|
+
if (issueId) {
|
|
82
|
+
out.issueId = resolveIssueRefFromString({ ref: issueId, defaultRepo: defaultIssueRepo() }).id;
|
|
83
|
+
}
|
|
84
|
+
const unsupported = ["agentId", "taskId", "landingRef"].filter((key) => cleanString(ref[key], `refs.${key}`, { max: 240 }));
|
|
85
|
+
if (unsupported.length > 0) throw new ValidationError("agentId/taskId/landingRef refs are populated by live binding in slice c");
|
|
86
|
+
return Object.keys(out).length ? out : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultIssueRepo(): string | undefined {
|
|
90
|
+
return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function syncPlanNodeIssueAssociation(planId: string, node: PlanNode): void {
|
|
94
|
+
if (!node.refs?.issueId) return;
|
|
95
|
+
associateIssue(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removePlanNodeIssueAssociation(planId: string, node: PlanNode): void {
|
|
99
|
+
if (!node.refs?.issueId) return;
|
|
100
|
+
removeAssociation(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function syncChangedPlanNodeIssueAssociation(planId: string, before: PlanNode | undefined, after: PlanNode): void {
|
|
104
|
+
if (before?.refs?.issueId && before.refs.issueId !== after.refs?.issueId) {
|
|
105
|
+
removeAssociation(before.refs.issueId, "plan-node", planNodeIssueEntityId(planId, before.id));
|
|
106
|
+
}
|
|
107
|
+
if (after.refs?.issueId) syncPlanNodeIssueAssociation(planId, after);
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
function cleanBlackboardRefs(value: unknown, planTeamId?: string): string[] | undefined {
|
|
@@ -229,6 +261,7 @@ export function createPlan(input: PlanCreateInput, ctx: AuthContext): Plan {
|
|
|
229
261
|
edges,
|
|
230
262
|
createdBy: actor,
|
|
231
263
|
});
|
|
264
|
+
for (const node of plan.nodes) syncPlanNodeIssueAssociation(plan.id, node);
|
|
232
265
|
emitPlan("plan.created", plan, actor);
|
|
233
266
|
return plan;
|
|
234
267
|
}
|
|
@@ -238,6 +271,7 @@ export function addPlanNode(planId: string, nodeInput: PlanNodeInput, ctx: AuthC
|
|
|
238
271
|
const node = cleanNode(nodeInput, plan.teamId);
|
|
239
272
|
if (plan.nodes.some((item) => item.id === node.id)) throw new ValidationError(`node ${node.id} already exists`);
|
|
240
273
|
const updated = replacePlan({ ...plan, nodes: [...plan.nodes, node] }, actorId(ctx));
|
|
274
|
+
syncPlanNodeIssueAssociation(updated.id, node);
|
|
241
275
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
242
276
|
return updated;
|
|
243
277
|
}
|
|
@@ -260,6 +294,7 @@ export function updatePlanNode(planId: string, nodeId: string, patch: PlanNodePa
|
|
|
260
294
|
};
|
|
261
295
|
const next = applyBoundStatus(base);
|
|
262
296
|
const updated = replacePlan({ ...plan, nodes: plan.nodes.map((node) => node.id === nodeId ? next : node) }, actorId(ctx));
|
|
297
|
+
syncChangedPlanNodeIssueAssociation(updated.id, existing, next);
|
|
263
298
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
264
299
|
return updated;
|
|
265
300
|
}
|
|
@@ -280,12 +315,14 @@ export function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext
|
|
|
280
315
|
|
|
281
316
|
export function removePlanNode(planId: string, nodeId: string, ctx: AuthContext): Plan {
|
|
282
317
|
const plan = existingPlan(planId);
|
|
283
|
-
|
|
318
|
+
const removed = plan.nodes.find((node) => node.id === nodeId);
|
|
319
|
+
if (!removed) throw new ValidationError(`node ${nodeId} not found`);
|
|
284
320
|
const updated = replacePlan({
|
|
285
321
|
...plan,
|
|
286
322
|
nodes: plan.nodes.filter((node) => node.id !== nodeId),
|
|
287
323
|
edges: plan.edges.filter((edge) => edge.from !== nodeId && edge.to !== nodeId),
|
|
288
324
|
}, actorId(ctx));
|
|
325
|
+
removePlanNodeIssueAssociation(plan.id, removed);
|
|
289
326
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
290
327
|
return updated;
|
|
291
328
|
}
|
|
@@ -304,6 +341,7 @@ export function deletePlanGraph(planId: string, ctx: AuthContext): { id: string;
|
|
|
304
341
|
const plan = existingPlan(planId);
|
|
305
342
|
const actor = actorId(ctx);
|
|
306
343
|
deletePlan(plan.id, actor);
|
|
344
|
+
for (const node of plan.nodes) removePlanNodeIssueAssociation(plan.id, node);
|
|
307
345
|
emitPlan("plan.deleted", plan, actor);
|
|
308
346
|
return { id: plan.id, deleted: true };
|
|
309
347
|
}
|
package/src/services/team.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dissolveTeam, ensureTeam, getAgent, getPlanByTeam, getTeam, getTeamRoster, setTeamBrief } from "../db";
|
|
2
2
|
import { emitRelayEvent } from "../events";
|
|
3
|
+
import { listIssueRefsForEntity } from "./issue-associations";
|
|
3
4
|
import type { AuthContext } from "./auth-context";
|
|
4
5
|
import type { Team } from "../types";
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ export interface TeamPayload {
|
|
|
12
13
|
brief: string | null;
|
|
13
14
|
definitionOfDone: string | null;
|
|
14
15
|
plan: ReturnType<typeof getPlanByTeam>;
|
|
16
|
+
issues: ReturnType<typeof listIssueRefsForEntity>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface TeamBriefInput {
|
|
@@ -41,6 +43,7 @@ function payloadForAgent(agentId: string, channel?: string): TeamPayload {
|
|
|
41
43
|
brief: team.brief ?? null,
|
|
42
44
|
definitionOfDone: team.definitionOfDone ?? null,
|
|
43
45
|
plan: getPlanByTeam(team.id, channel) ?? null,
|
|
46
|
+
issues: listIssueRefsForEntity("team", team.id),
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -85,5 +88,6 @@ export function dissolveCallerTeam(ctx: AuthContext, input: TeamDissolveServiceI
|
|
|
85
88
|
brief: team.brief ?? null,
|
|
86
89
|
definitionOfDone: team.definitionOfDone ?? null,
|
|
87
90
|
plan: getPlanByTeam(team.id) ?? null,
|
|
91
|
+
issues: listIssueRefsForEntity("team", team.id),
|
|
88
92
|
};
|
|
89
93
|
}
|
package/src/token-db.ts
CHANGED
|
@@ -77,7 +77,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
77
77
|
name: "Provider Agent",
|
|
78
78
|
description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
|
|
79
79
|
role: "provider",
|
|
80
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
|
|
80
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
|
|
81
81
|
ttlSeconds: 24 * 60 * 60,
|
|
82
82
|
builtIn: true,
|
|
83
83
|
createdBy: "system",
|
|
@@ -87,7 +87,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
87
87
|
name: "Provider Child Agent",
|
|
88
88
|
description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
|
|
89
89
|
role: "provider",
|
|
90
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
|
|
90
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
|
|
91
91
|
constraints: { canDelegate: false },
|
|
92
92
|
ttlSeconds: 2 * 60 * 60,
|
|
93
93
|
builtIn: true,
|
|
@@ -98,7 +98,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
98
98
|
name: "Provider Interactive Agent",
|
|
99
99
|
description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
|
|
100
100
|
role: "provider",
|
|
101
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
|
|
101
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
|
|
102
102
|
constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
|
|
103
103
|
ttlSeconds: 30 * 24 * 60 * 60,
|
|
104
104
|
builtIn: true,
|
package/src/workspace-actions.ts
CHANGED
|
@@ -24,7 +24,8 @@ import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
|
|
|
24
24
|
// tuple as the HTTP route without a second import hop (one source: workspace-merge, #304).
|
|
25
25
|
export { LAND_STRATEGIES, DEFAULT_MERGE_STRATEGY } from "./workspace-merge";
|
|
26
26
|
import { claimMetadataPatch, workspaceActiveClaim } from "./workspace-claim";
|
|
27
|
-
import { TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
|
|
27
|
+
import { READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
|
|
28
|
+
import { scheduleRepoMerge } from "./workspace-auto-merge";
|
|
28
29
|
import type { Command, WorkspaceAutoMergePolicy, WorkspaceMergeStrategy, WorkspaceRecord, WorkspaceStatus } from "./types";
|
|
29
30
|
|
|
30
31
|
// Single source of truth for the action verb set. The route's `optionalEnum` and
|
|
@@ -239,6 +240,12 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
|
|
|
239
240
|
});
|
|
240
241
|
if (!updated) return { ok: false, httpStatus: 404, error: "workspace not found" };
|
|
241
242
|
|
|
243
|
+
// #237: a ready/request-review hand-off kicks the per-repo auto-merge inline right
|
|
244
|
+
// then, instead of waiting up to the backstop interval — this is what makes "mark
|
|
245
|
+
// ready → land" take seconds, not minutes (the #235 root-cause fix). Fire-and-forget
|
|
246
|
+
// + lease-serialized; a non-landable row is a harmless no-op (autoMergeForRepo re-checks).
|
|
247
|
+
if (READY_TO_LAND_STATUSES.has(nextStatus)) scheduleRepoMerge(updated.repoRoot);
|
|
248
|
+
|
|
242
249
|
let command: Command | undefined;
|
|
243
250
|
if (requiresCommand) {
|
|
244
251
|
// Only `cleanup` reaches here — `merge` returned early via the shared helper.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Event-driven workspace merge-back engine (#237).
|
|
2
|
+
//
|
|
3
|
+
// The branch merge-back used to be driven entirely by a 2-min maintenance poll, so an
|
|
4
|
+
// agent that marked ready waited an invisible up-to-2-min interval with no signal —
|
|
5
|
+
// the "feels like a hung process" UX that was #235's root cause. This module makes the
|
|
6
|
+
// land path EVENT-DRIVEN and demotes the poll to a self-healing backstop (the standard
|
|
7
|
+
// controller pattern: react to events, periodically resync):
|
|
8
|
+
//
|
|
9
|
+
// - on `ready`/`request-review` → `scheduleRepoMerge(repoRoot)` (workspace-actions.ts)
|
|
10
|
+
// - on a merge command settling → `scheduleRepoMerge(repoRoot)` (routes/commands.ts)
|
|
11
|
+
// - periodic backstop + runOnStart → `autoMergeCleanFastForwards` (maintenance.ts def)
|
|
12
|
+
//
|
|
13
|
+
// `landCandidates` is THE single per-candidate land engine; all three entry points
|
|
14
|
+
// funnel through it so the land rule has one home. Dispatch is serialized by the
|
|
15
|
+
// per-repo merge lease inside `requestWorkspaceMerge`, so a single pass dispatches at
|
|
16
|
+
// most one merge per repo — the rest report `heldByLease` and are drained by the
|
|
17
|
+
// on-settle re-trigger. That chain (schedule → dispatch → on-settle re-schedule → next)
|
|
18
|
+
// IS the "sequential drain under the lease" the design calls for; no new concurrency
|
|
19
|
+
// model. Different repos run on independent leases, so they land in parallel.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createActivityEvent,
|
|
23
|
+
getActiveMergePause,
|
|
24
|
+
listOrchestrators,
|
|
25
|
+
listWorkspaces,
|
|
26
|
+
patchWorkspaceMetadata,
|
|
27
|
+
} from "./db";
|
|
28
|
+
import { emitCommandEvent } from "./command-events";
|
|
29
|
+
import { getLandingConfig, getStewardConfig } from "./config-store";
|
|
30
|
+
import { getLifecycleManager } from "./lifecycle-manager";
|
|
31
|
+
import { routeNotification } from "./notification-router";
|
|
32
|
+
import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
|
|
33
|
+
import { ensureRepoSteward } from "./steward";
|
|
34
|
+
import { requestWorkspaceMerge } from "./workspace-merge";
|
|
35
|
+
import { workspaceActiveClaim } from "./workspace-claim";
|
|
36
|
+
import { DIRTY_WORKTREE_LAND_SKIP_REASON, READY_TO_LAND_STATUSES } from "./workspace-phase";
|
|
37
|
+
import { fetchHostMergePreview, workspacePathWithinBase } from "./workspace-probe";
|
|
38
|
+
import type { WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
39
|
+
|
|
40
|
+
// Don't re-wake the managed steward for the same workspace more than once per this
|
|
41
|
+
// window — a persistent conflict/behind row would otherwise re-ping every sweep.
|
|
42
|
+
const STEWARD_WAKE_COOLDOWN_MS = Number(process.env.AGENT_RELAY_STEWARD_WAKE_COOLDOWN_MS) || 10 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
// Wake the managed per-repo steward (issue #167) for a workspace it should handle:
|
|
45
|
+
// auto-provision the policy from global steward config, then queue a `policy:` wake
|
|
46
|
+
// message (which also spawns the on-demand agent now via onMessageForPolicy). Honors a
|
|
47
|
+
// per-workspace cooldown so a persistent conflict/behind row isn't re-pinged every sweep.
|
|
48
|
+
// Returns the steward policy name on a fresh wake, or null (disabled / no owner / cooled down).
|
|
49
|
+
export function wakeRepoSteward(ws: WorkspaceRecord, reason: string): string | null {
|
|
50
|
+
const meta = ws.metadata as Record<string, unknown>;
|
|
51
|
+
const lastWoke = typeof meta.stewardWokenAt === "number" ? meta.stewardWokenAt : 0;
|
|
52
|
+
if (lastWoke && Date.now() - lastWoke < STEWARD_WAKE_COOLDOWN_MS) return null;
|
|
53
|
+
const policyName = ensureRepoSteward(ws.repoRoot);
|
|
54
|
+
if (!policyName) return null;
|
|
55
|
+
try {
|
|
56
|
+
// Obligation (ttl null by type default): a steward task must survive until claimed/handled.
|
|
57
|
+
routeNotification({
|
|
58
|
+
type: "workspace.steward_task",
|
|
59
|
+
scope: { workspaceId: ws.id, repoRoot: ws.repoRoot },
|
|
60
|
+
recipients: [`policy:${policyName}`],
|
|
61
|
+
message: {
|
|
62
|
+
subject: `Steward: ${ws.status} workspace needs attention`,
|
|
63
|
+
body: `Workspace \`${ws.branch ?? ws.id}\` (id ${ws.id}) in ${ws.repoRoot} is ${ws.status} and could not auto-land (${reason}). Claim it first so auto-merge yields: \`agent-relay workspace claim --id ${ws.id} --purpose steward\`. Inspect: \`agent-relay steward inspect ${ws.id}\`. Then cd into ${ws.worktreePath}, rebase onto ${ws.baseRef ?? "base"}, resolve, run checks, and land: \`agent-relay workspace land --id ${ws.id} --strategy rebase-ff\` — or \`agent-relay workspace release --id ${ws.id}\` and escalate if you can't.`,
|
|
64
|
+
payload: { kind: "workspace.steward-task", workspaceId: ws.id, repoRoot: ws.repoRoot, worktreePath: ws.worktreePath, branch: ws.branch, baseRef: ws.baseRef, status: ws.status, reason },
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
getLifecycleManager().onMessageForPolicy(policyName);
|
|
68
|
+
patchWorkspaceMetadata(ws.id, { stewardWokenAt: Date.now(), stewardPolicy: policyName });
|
|
69
|
+
return policyName;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface LandSweepResult {
|
|
76
|
+
scanned: number;
|
|
77
|
+
merged: string[];
|
|
78
|
+
heldByLease: string[];
|
|
79
|
+
heldByClaim: string[];
|
|
80
|
+
heldByPause: string[];
|
|
81
|
+
heldAwaitingApproval: string[];
|
|
82
|
+
leftForSteward: string[];
|
|
83
|
+
wokeStewards: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function emptyLandSweep(): LandSweepResult {
|
|
87
|
+
return { scanned: 0, merged: [], heldByLease: [], heldByClaim: [], heldByPause: [], heldAwaitingApproval: [], leftForSteward: [], wokeStewards: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Deterministic auto-land (Layer 0, #167/#207/#242): land conflict-free ready work
|
|
91
|
+
// under the shared per-repo lease, rebasing when base moved; leave uncertainty to
|
|
92
|
+
// stewards. THE single per-candidate land engine — see module header.
|
|
93
|
+
async function landCandidates(
|
|
94
|
+
candidates: WorkspaceRecord[],
|
|
95
|
+
orchestrators: ReturnType<typeof listOrchestrators>,
|
|
96
|
+
): Promise<LandSweepResult> {
|
|
97
|
+
const stewardEnabled = getStewardConfig().enabled;
|
|
98
|
+
const merged: string[] = [];
|
|
99
|
+
const heldByLease: string[] = [], heldByClaim: string[] = [], heldByPause: string[] = [], heldAwaitingApproval: string[] = [], leftForSteward: string[] = [], wokeStewards: string[] = [];
|
|
100
|
+
|
|
101
|
+
for (const ws of candidates) {
|
|
102
|
+
if (isAwaitingReviewerApproval(ws)) { heldAwaitingApproval.push(ws.id); continue; }
|
|
103
|
+
// A claimed workspace is being validated by a steward — don't race it (#208).
|
|
104
|
+
if (workspaceActiveClaim(ws)) { heldByClaim.push(ws.id); continue; }
|
|
105
|
+
if (getActiveMergePause(ws.repoRoot, ws.baseRef ?? "main")) { heldByPause.push(ws.id); continue; }
|
|
106
|
+
const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
|
|
107
|
+
if (!orch?.apiUrl) continue;
|
|
108
|
+
const preview = await fetchHostMergePreview(orch.apiUrl, ws);
|
|
109
|
+
if (!preview || (preview as { available?: false }).available === false) continue;
|
|
110
|
+
const p = preview as WorkspaceMergePreview;
|
|
111
|
+
if (p.error || p.missing) continue;
|
|
112
|
+
if (p.reason === DIRTY_WORKTREE_LAND_SKIP_REASON) {
|
|
113
|
+
patchWorkspaceMetadata(ws.id, { lastLandSkipReason: p.reason, lastLandSkipAt: Date.now() });
|
|
114
|
+
} else if (ws.metadata.lastLandSkipReason === DIRTY_WORKTREE_LAND_SKIP_REASON) {
|
|
115
|
+
patchWorkspaceMetadata(ws.id, { lastLandSkipReason: undefined, lastLandSkipAt: undefined });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ahead = p.unmergedAhead ?? p.ahead ?? 0;
|
|
119
|
+
const behind = p.behind ?? 0;
|
|
120
|
+
// Land anything that won't conflict — including a base that moved on (behind>0)
|
|
121
|
+
// but rebases cleanly. The merge-tree prediction already accounts for the moved
|
|
122
|
+
// base, and mergeRebaseFf rebases then aborts-to-conflict on a real replay
|
|
123
|
+
// conflict, so a too-optimistic prediction degrades safely to review_requested.
|
|
124
|
+
// Only a predicted conflict (true) or unknown state (undefined) goes to the steward.
|
|
125
|
+
// A no-op (#230) — nothing to land (ahead=0, clean) — is also landable: the host
|
|
126
|
+
// resolves it to a terminal `merged` status, draining the queue instead of waking
|
|
127
|
+
// the steward every sweep for a workspace with no work.
|
|
128
|
+
const canLand = p.noop === true || (p.conflict === false && ahead > 0);
|
|
129
|
+
if (!canLand) {
|
|
130
|
+
leftForSteward.push(ws.id);
|
|
131
|
+
if (stewardEnabled) {
|
|
132
|
+
const woke = wakeRepoSteward(ws, p.conflict === true ? "conflict" : "merge state unknown");
|
|
133
|
+
if (woke) wokeStewards.push(woke);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Honor the operator's instance landing.strategy (#419-followup): when PR mode is on
|
|
139
|
+
// (`landing.strategy = "pr"`), autonomous worker lands must open a PR too — not silently
|
|
140
|
+
// direct-merge. Passing an explicit "rebase-ff" here SHADOWS the instance config inside
|
|
141
|
+
// requestWorkspaceMerge (explicit wins over instanceStrategy), which is the bug that made
|
|
142
|
+
// PR mode never engage for spawned workers. For "pr" we OMIT the explicit strategy so
|
|
143
|
+
// requestWorkspaceMerge resolves it from the config and defaults autoMerge to "on-green";
|
|
144
|
+
// the default "direct" keeps the historical deterministic rebase-ff (no behavior change).
|
|
145
|
+
const result = requestWorkspaceMerge(ws, getLandingConfig().strategy === "pr" ? { requestedBy: "auto-merge" } : { strategy: "rebase-ff", requestedBy: "auto-merge" });
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
// 409 = another merge holds this repo's lease this tick; the on-settle re-trigger
|
|
148
|
+
// (routes/commands.ts → scheduleRepoMerge) drains it once the lease frees.
|
|
149
|
+
heldByLease.push(ws.id);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
emitCommandEvent(result.command, "command.requested");
|
|
153
|
+
merged.push(ws.id);
|
|
154
|
+
createActivityEvent({
|
|
155
|
+
clientId: `workspace-auto-merge-${ws.id}-${Date.now()}`,
|
|
156
|
+
kind: "state",
|
|
157
|
+
title: p.noop ? "Workspace auto-resolving (no work to merge)" : behind > 0 ? "Workspace auto-merging (rebase)" : "Workspace auto-merging (clean fast-forward)",
|
|
158
|
+
body: p.noop
|
|
159
|
+
? `${ws.branch ?? ws.id} → ${p.baseRef ?? "base"} (nothing to land — resolving to merged)`
|
|
160
|
+
: `${ws.branch ?? ws.id} → ${p.baseRef ?? "base"} (${ahead} ahead${behind > 0 ? `, ${behind} behind — rebasing` : ", clean"})`,
|
|
161
|
+
meta: ws.branch ?? ws.id,
|
|
162
|
+
icon: "ti-git-merge",
|
|
163
|
+
view: "orchestrators",
|
|
164
|
+
metadata: { source: "server", maintenanceJobId: "workspace-auto-merge", workspaceId: ws.id, commandId: result.command.id, ahead },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { scanned: candidates.length, merged, heldByLease, heldByClaim, heldByPause, heldAwaitingApproval, leftForSteward, wokeStewards };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Coalesced, fire-and-forget per-repo land trigger (see module header). A run already
|
|
172
|
+
// in flight for the repo just flags a rerun, so concurrent readies don't fan out
|
|
173
|
+
// redundant host probes. Errors are swallowed: the runOnStart backstop is the
|
|
174
|
+
// self-healer for anything a dropped run strands.
|
|
175
|
+
const repoMergeRunning = new Map<string, { rerun: boolean }>();
|
|
176
|
+
|
|
177
|
+
export function scheduleRepoMerge(repoRoot: string | undefined): void {
|
|
178
|
+
if (!repoRoot) return;
|
|
179
|
+
if (process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE === "0") return;
|
|
180
|
+
const existing = repoMergeRunning.get(repoRoot);
|
|
181
|
+
if (existing) { existing.rerun = true; return; }
|
|
182
|
+
const state = { rerun: false };
|
|
183
|
+
repoMergeRunning.set(repoRoot, state);
|
|
184
|
+
void (async () => {
|
|
185
|
+
try {
|
|
186
|
+
do {
|
|
187
|
+
state.rerun = false;
|
|
188
|
+
await autoMergeForRepo(repoRoot);
|
|
189
|
+
} while (state.rerun);
|
|
190
|
+
} catch {
|
|
191
|
+
// Swallow — the periodic backstop (runOnStart + interval) is the self-healer.
|
|
192
|
+
} finally {
|
|
193
|
+
repoMergeRunning.delete(repoRoot);
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resetRepoMergeSchedulerForTests(): void { repoMergeRunning.clear(); }
|
|
199
|
+
|
|
200
|
+
// Drain the landable rows for ONE repo (the event-driven fast path). Scoped to the
|
|
201
|
+
// repo whose ready/settle event fired so it doesn't re-probe every other repo's worktrees.
|
|
202
|
+
async function autoMergeForRepo(repoRoot: string): Promise<LandSweepResult> {
|
|
203
|
+
if (process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE === "0") return emptyLandSweep();
|
|
204
|
+
const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
|
|
205
|
+
if (!orchestrators.length) return emptyLandSweep();
|
|
206
|
+
const candidates = listWorkspaces({ repoRoot }).filter(
|
|
207
|
+
(ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && READY_TO_LAND_STATUSES.has(ws.status),
|
|
208
|
+
);
|
|
209
|
+
return landCandidates(candidates, orchestrators);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Self-healing backstop (#237): the periodic resync (workspace-auto-merge maintenance
|
|
213
|
+
// job, runOnStart + ~10-min interval) that lands anything events missed — a relay
|
|
214
|
+
// restart that lost the in-flight chain, a dropped stop-hook, a direct human commit
|
|
215
|
+
// with no agent turn. Scans EVERY repo's landable rows; the per-repo lease still caps
|
|
216
|
+
// it at one dispatch per repo per sweep, and the on-settle re-trigger drains the rest fast.
|
|
217
|
+
export async function autoMergeCleanFastForwards(): Promise<Record<string, unknown>> {
|
|
218
|
+
if (process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE === "0") return { skipped: "disabled" };
|
|
219
|
+
const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
|
|
220
|
+
if (!orchestrators.length) return { scanned: 0, skipped: "no online orchestrators" };
|
|
221
|
+
|
|
222
|
+
const candidates = listWorkspaces().filter(
|
|
223
|
+
(ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && READY_TO_LAND_STATUSES.has(ws.status),
|
|
224
|
+
);
|
|
225
|
+
// Spread to a plain Record for the maintenance-job result contract (the named
|
|
226
|
+
// LandSweepResult interface has no index signature).
|
|
227
|
+
return { ...(await landCandidates(candidates, orchestrators)) };
|
|
228
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Single-workspace git-state probe (#237 / #236). The relay isn't in the git path,
|
|
2
|
+
// so the only way it learns a worktree's ahead/dirty counts is to ask the owning
|
|
3
|
+
// host. Two callers share this:
|
|
4
|
+
// - the stop-hook handler (`probeWorkspaceState`, fired on turn-end in bus.ts) —
|
|
5
|
+
// the instant, scoped path that flips the ⚪↔🟡 changes badge the moment work
|
|
6
|
+
// appears, instead of waiting up to the backstop scan interval.
|
|
7
|
+
// - the periodic conflict scan (maintenance.ts) — the self-healing backstop that
|
|
8
|
+
// re-derives the same freshness for every worktree.
|
|
9
|
+
// Both funnel the "apply the host's ahead/dirty counts to the row + emit on flip"
|
|
10
|
+
// rule through `applyBranchFreshness` so it can't drift between the two surfaces.
|
|
11
|
+
|
|
12
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
13
|
+
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
14
|
+
import {
|
|
15
|
+
emitWorkspaceChange,
|
|
16
|
+
listOrchestrators,
|
|
17
|
+
ownedIsolatedWorkspace,
|
|
18
|
+
patchWorkspaceMetadata,
|
|
19
|
+
} from "./db";
|
|
20
|
+
import { deriveBranchState } from "./workspace-phase";
|
|
21
|
+
import type { AgentCard, WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
22
|
+
|
|
23
|
+
/** True when `path` is the base dir itself or nested inside it (containment via
|
|
24
|
+
* resolve+relative, never string startsWith — see CLAUDE.md path-containment rule). */
|
|
25
|
+
export function workspacePathWithinBase(path: string | undefined, baseDir: string | undefined): boolean {
|
|
26
|
+
if (!path || !baseDir) return false;
|
|
27
|
+
const rel = relative(resolve(baseDir), resolve(path));
|
|
28
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Ask the owning host whether a worktree can still merge cleanly (+ ahead/dirty/PR
|
|
32
|
+
* state). Returns null on any transport/host failure — callers skip rather than act
|
|
33
|
+
* on incomplete data. `checkPr` costs a `gh` round-trip, so badge probes opt out. */
|
|
34
|
+
export async function fetchHostMergePreview(
|
|
35
|
+
apiUrl: string,
|
|
36
|
+
workspace: WorkspaceRecord,
|
|
37
|
+
opts: { checkPr?: boolean } = {},
|
|
38
|
+
): Promise<WorkspaceMergePreview | { available: false } | null> {
|
|
39
|
+
const checkPr = opts.checkPr !== false;
|
|
40
|
+
const query = new URLSearchParams({ path: workspace.worktreePath, ...(checkPr ? { checkPr: "1" } : {}) });
|
|
41
|
+
if (workspace.baseRef) query.set("baseRef", workspace.baseRef);
|
|
42
|
+
if (workspace.baseSha) query.set("baseSha", workspace.baseSha);
|
|
43
|
+
const headers: Record<string, string> = {};
|
|
44
|
+
const token = process.env.AGENT_RELAY_TOKEN;
|
|
45
|
+
if (token) headers[RELAY_TOKEN_HEADER] = token;
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${apiUrl}/api/workspace/merge-preview?${query.toString()}`, { headers, signal: AbortSignal.timeout(8_000) });
|
|
48
|
+
if (!res.ok) return null;
|
|
49
|
+
return await res.json() as WorkspaceMergePreview;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Stash the host's latest ahead/dirty counts on the workspace row so
|
|
57
|
+
* `deriveBranchState` can tell idle (⚪) from changes (🟡). Only writes (and only
|
|
58
|
+
* refreshes the owner's badge over SSE) when the DERIVED state actually flips, so an
|
|
59
|
+
* unchanged worktree never churns SSE. Returns true when the badge flipped.
|
|
60
|
+
*/
|
|
61
|
+
export function applyBranchFreshness(
|
|
62
|
+
ws: WorkspaceRecord,
|
|
63
|
+
p: Pick<WorkspaceMergePreview, "ahead" | "dirtyCount">,
|
|
64
|
+
): boolean {
|
|
65
|
+
const nextAhead = p.ahead ?? 0;
|
|
66
|
+
const nextDirty = p.dirtyCount ?? 0;
|
|
67
|
+
const meta = ws.metadata as Record<string, unknown>;
|
|
68
|
+
if (meta.gitAhead === nextAhead && meta.gitDirtyCount === nextDirty) return false;
|
|
69
|
+
const before = deriveBranchState(ws);
|
|
70
|
+
const patched = patchWorkspaceMetadata(ws.id, { gitAhead: nextAhead, gitDirtyCount: nextDirty, gitProbedAt: Date.now() });
|
|
71
|
+
if (patched && deriveBranchState(patched) !== before) {
|
|
72
|
+
emitWorkspaceChange(patched);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ProbeResult {
|
|
79
|
+
/** True when the host preview came back and freshness was applied. */
|
|
80
|
+
probed: boolean;
|
|
81
|
+
/** True when the derived branch-state badge flipped (⚪↔🟡). */
|
|
82
|
+
flipped?: boolean;
|
|
83
|
+
/** The probed workspace id, when the agent owns a live isolated worktree. */
|
|
84
|
+
workspaceId?: string;
|
|
85
|
+
/** Why the probe was a no-op, for diagnostics. */
|
|
86
|
+
skipped?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Probe THIS agent's branch worktree on turn-end (#237). Single scoped host round-trip
|
|
91
|
+
* (not the full fan-out scan) so the changes badge flips at the exact moment work
|
|
92
|
+
* appears. Only the `active` state carries the ⚪↔🟡 distinction — handed-off states
|
|
93
|
+
* (`ready`/`review_requested`/…) derive their badge without git counts — so we skip
|
|
94
|
+
* the host round-trip otherwise. Never throws: a probe failure must not disturb the
|
|
95
|
+
* turn-end path; the backstop scan re-derives it later.
|
|
96
|
+
*/
|
|
97
|
+
export async function probeWorkspaceState(agentId: string): Promise<ProbeResult> {
|
|
98
|
+
const ws = ownedIsolatedWorkspace(agentId);
|
|
99
|
+
if (!ws || !ws.worktreePath) return { probed: false, skipped: "no live isolated workspace" };
|
|
100
|
+
if (ws.status !== "active") return { probed: false, workspaceId: ws.id, skipped: `status ${ws.status} derives badge without git counts` };
|
|
101
|
+
const orch = listOrchestrators().find(
|
|
102
|
+
(candidate) => candidate.status === "online" && candidate.apiUrl && workspacePathWithinBase(ws.sourceCwd, candidate.baseDir),
|
|
103
|
+
);
|
|
104
|
+
if (!orch?.apiUrl) return { probed: false, workspaceId: ws.id, skipped: "no online orchestrator owns this path" };
|
|
105
|
+
// checkPr:false — a badge probe never needs the gh round-trip (#304).
|
|
106
|
+
const preview = await fetchHostMergePreview(orch.apiUrl, ws, { checkPr: false });
|
|
107
|
+
if (!preview || (preview as { available?: false }).available === false) return { probed: false, workspaceId: ws.id, skipped: "host preview unavailable" };
|
|
108
|
+
const p = preview as WorkspaceMergePreview;
|
|
109
|
+
if (p.error || p.missing) return { probed: false, workspaceId: ws.id, skipped: "host preview error" };
|
|
110
|
+
const flipped = applyBranchFreshness(ws, p);
|
|
111
|
+
return { probed: true, flipped, workspaceId: ws.id };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Turn-end edge guard for the bus stop-hook (#237). A status frame that takes an
|
|
116
|
+
* agent OUT of `busy` is its turn-end signal; probe that one agent's worktree right
|
|
117
|
+
* then so the ⚪↔🟡 changes badge flips at turn-stop instead of waiting for the backstop
|
|
118
|
+
* scan. Guarded to the busy→non-busy edge so we don't fire a host round-trip on every
|
|
119
|
+
* heartbeat, and fire-and-forget (probeWorkspaceState never throws) so the turn-end
|
|
120
|
+
* path is never blocked. Lives here (not in bus.ts) so the probe decision has one home.
|
|
121
|
+
*/
|
|
122
|
+
export function probeWorkspaceOnTurnEnd(before: AgentCard | null | undefined, after: AgentCard | null | undefined): void {
|
|
123
|
+
if (!after || before?.status !== "busy" || after.status === "busy") return;
|
|
124
|
+
void probeWorkspaceState(after.id);
|
|
125
|
+
}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{In as t,Jn as n,Pn as r,ln as i,on as a,wt as o}from"./lucide-react-L2UxFDic.js";import{n as s,r as c,t as l}from"./store-B8xP6z6T.js";var u=e(n(),1),d=`(max-width: 767px)`;function f(e){if(typeof window>`u`)return()=>{};let t=window.matchMedia(d);return t.addEventListener(`change`,e),()=>t.removeEventListener(`change`,e)}function p(){return(0,u.useSyncExternalStore)(f,()=>typeof window<`u`?window.matchMedia(d).matches:!1,()=>!1)}var m=t(),h=196,g=68,_=52,v=16,y=14,b=[`landed`,`in-progress`,`pending`,`blocked`],x={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 S(e){return e?e.label||(e.policyName?`policy:${e.policyName}`:e.agentId||``):``}function C(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=g+v,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:y+r*(h+_),y:y+a+n*f})})});let m=u.length;return{nodes:p,width:Math.max(320,y*2+m*h+Math.max(0,m-1)*_),height:Math.max(g+y*2,y*2+d*g+Math.max(0,d-1)*v)}}function w(e,t){let n=e.x+h,r=e.y+g/2,i=t.x,a=t.y+g/2,o=n+Math.max(20,(i-n)/2);return`M ${n} ${r} C ${o} ${r}, ${o} ${a}, ${i} ${a}`}function T({threadId:e,teamId:t,channel:n}){let d=t?c(t,n):e?s(e,n):``,f=l(e=>t?e.plansByTeam[d]:d?e.plansByThread[d]:null),_=l(e=>e.fetchPlan),v=l(e=>e.planCollapsed),y=l(e=>e.setPlanCollapsed),T=p(),E=v??T;(0,u.useEffect)(()=>{if(t){_({teamId:t,channel:n});return}e&&_({threadId:e,channel:n})},[n,_,t,e]);let D=(0,u.useMemo)(()=>f?C(f):null,[f]),O=(0,u.useMemo)(()=>new Map(D?.nodes.map(e=>[e.node.id,e])??[]),[D]),k=(0,u.useMemo)(()=>{if(!f)return[];let e=new Map;for(let t of f.nodes)e.set(t.status,(e.get(t.status)??0)+1);return b.filter(t=>e.has(t)).map(t=>({status:t,count:e.get(t)??0}))},[f]);if(!f||!D)return null;let A=f.nodes.length,j=k.find(e=>e.status===`landed`)?.count??0,M=`${A} node${A===1?``:`s`}${j>0?` · ${j} landed`:``}`;return(0,m.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,m.jsxs)(`button`,{type:`button`,onClick:()=>y(!E),"aria-expanded":!E,"aria-label":E?`Expand plan graph`:`Collapse plan graph`,className:r(`flex w-full min-w-0 items-center gap-2 text-left`,!E&&`mb-2`),children:[(0,m.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,m.jsx)(o,{className:`h-3 w-3 text-muted-foreground`})}),(0,m.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,m.jsx)(`div`,{className:`truncate text-[13px] font-semibold leading-tight text-foreground`,children:f.title||`Plan`}),(0,m.jsx)(`div`,{className:`truncate text-[11px] leading-tight text-muted-foreground`,children:E?M:f.objective||M})]}),k.length>0&&(0,m.jsx)(`div`,{className:`hidden shrink-0 items-center gap-2.5 sm:flex`,children:k.map(({status:e,count:t})=>(0,m.jsxs)(`span`,{className:`flex items-center gap-1 text-[10px] text-muted-foreground`,children:[(0,m.jsx)(`span`,{className:r(`h-1.5 w-1.5 rounded-full`,x[e].dot)}),(0,m.jsx)(`span`,{className:r(`font-medium tabular-nums`,x[e].text),children:t}),(0,m.jsx)(`span`,{children:x[e].label})]},e))}),(0,m.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`,f.version]}),(0,m.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:E?(0,m.jsx)(i,{className:`h-3.5 w-3.5`}):(0,m.jsx)(a,{className:`h-3.5 w-3.5`})})]}),E?null:f.nodes.length===0?(0,m.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,m.jsx)(`div`,{className:`max-h-[min(46vh,360px)] overflow-auto`,children:(0,m.jsxs)(`div`,{className:`relative`,style:{width:D.width,height:D.height},children:[(0,m.jsxs)(`svg`,{className:`pointer-events-none absolute inset-0`,width:D.width,height:D.height,"aria-hidden":`true`,children:[(0,m.jsx)(`defs`,{children:b.map(e=>(0,m.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,m.jsx)(`path`,{d:`M0 0 L8 4 L0 8 z`,fill:x[e].wire})},e))}),f.edges.map(e=>{let t=O.get(e.from),n=O.get(e.to);if(!t||!n)return null;let r=x[t.node.status];return(0,m.jsx)(`path`,{d:w(t,n),stroke:r.wire,strokeOpacity:.7,strokeWidth:2,fill:`none`,strokeLinecap:`round`,markerEnd:`url(#plan-arrow-${t.node.status})`},`${e.from}->${e.to}`)})]}),D.nodes.map(({node:e,x:t,y:n})=>{let i=x[e.status],a=S(e.ownerRef);return(0,m.jsxs)(`article`,{className:r(`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`,i.accent),style:{left:t,top:n,width:h,height:g},title:e.scopeBlurb||e.label,children:[(0,m.jsx)(`div`,{className:`truncate text-xs font-semibold leading-tight text-foreground`,children:e.label}),(e.scopeBlurb||e.id)&&(0,m.jsx)(`div`,{className:`truncate text-[10px] leading-tight text-muted-foreground`,children:e.scopeBlurb||e.id}),(0,m.jsxs)(`div`,{className:`flex min-w-0 items-center justify-between gap-1.5`,children:[(0,m.jsxs)(`div`,{className:`flex shrink-0 items-center gap-1.5`,children:[(0,m.jsx)(`span`,{className:r(`h-1.5 w-1.5 shrink-0 rounded-full`,i.dot)}),(0,m.jsx)(`span`,{className:r(`text-[10px] font-medium`,i.text),children:i.label})]}),(0,m.jsxs)(`div`,{className:`flex min-w-0 items-center gap-1.5`,children:[a&&(0,m.jsx)(`span`,{className:`truncate text-[10px] text-muted-foreground`,children:a}),e.lane&&(0,m.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)})]})})]})}export{T as t};
|
|
2
|
-
//# sourceMappingURL=PlanGraphPanel-Dby7wq9c.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PlanGraphPanel-Dby7wq9c.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 } from 'react'\nimport { ChevronDown, ChevronUp, GitBranch } from 'lucide-react'\nimport { useRelayStore } from '@/store'\nimport type { Plan, PlanNode, PlanOwnerRef, PlanStatus } from '@/types'\nimport { cn } from '@/lib/utils'\nimport { planLookupKey, planTeamLookupKey } from '@/store/slices/plans'\nimport { useIsMobile } from './hooks/useIsMobile'\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 = 68\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 planCollapsed = useRelayStore((s) => s.planCollapsed)\n const setPlanCollapsed = useRelayStore((s) => s.setPlanCollapsed)\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 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 <button\n type=\"button\"\n onClick={() => setPlanCollapsed(!collapsed)}\n aria-expanded={!collapsed}\n aria-label={collapsed ? 'Expand plan graph' : 'Collapse plan graph'}\n className={cn(\n 'flex w-full min-w-0 items-center gap-2 text-left',\n !collapsed && 'mb-2',\n )}\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 {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 <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 {collapsed ? <ChevronDown className=\"h-3.5 w-3.5\" /> : <ChevronUp className=\"h-3.5 w-3.5\" />}\n </span>\n </button>\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 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 <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"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import"./chunk-CilyBKbf.js";import{In as e,Jn as t,Pn as n}from"./lucide-react-L2UxFDic.js";t();var r=e();function i({className:e,size:t=`default`,...i}){return(0,r.jsx)(`div`,{"data-slot":`card`,"data-size":t,className:n(`group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl`,e),...i})}function a({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-header`,className:n(`group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3`,e),...t})}function o({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-title`,className:n(`font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm`,e),...t})}function s({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-description`,className:n(`text-sm text-muted-foreground`,e),...t})}function c({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-action`,className:n(`col-start-2 row-span-2 row-start-1 self-start justify-self-end`,e),...t})}function l({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-content`,className:n(`px-4 group-data-[size=sm]/card:px-3`,e),...t})}export{a,s as i,c as n,o,l as r,i as t};
|
|
2
|
-
//# sourceMappingURL=card-CcZ9hLI7.js.map
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{Dn as e,En as t,In as n,On as r,Rt as i,Y as a}from"./lucide-react-L2UxFDic.js";import{o,t as s}from"./store-B8xP6z6T.js";import{G as c,K as l,U as u,c as d,s as f}from"./display-DzNvOu1j.js";import{t as p}from"./badge-DhLyCPag.js";import{t as m}from"./button-K20wAZG1.js";import{a as h,o as g,r as _,t as v}from"./card-CcZ9hLI7.js";var y=n();function b({direction:n}){return n===`inbound`?(0,y.jsx)(e,{className:`w-3 h-3`}):n===`outbound`?(0,y.jsx)(t,{className:`w-3 h-3`}):(0,y.jsx)(r,{className:`w-3 h-3`})}function x({channel:e}){let t=o(),n=s(e=>e.openChannelDetail),r=d(e),f=e.status!==`offline`;return(0,y.jsxs)(v,{className:`${f?``:`opacity-60`} hover:border-zinc-600 transition-colors`,children:[(0,y.jsx)(h,{className:`pb-2 pt-4 px-4`,children:(0,y.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,y.jsx)(`div`,{className:`p-1.5 rounded-md ${f?`bg-blue-500/10`:`bg-zinc-800`}`,children:(0,y.jsx)(a,{className:`w-4 h-4 ${f?`text-blue-400`:`text-zinc-500`}`})}),(0,y.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,y.jsx)(g,{className:`text-sm font-medium truncate`,children:e.name||e.id}),(0,y.jsxs)(`div`,{className:`text-xs text-muted-foreground mt-0.5`,children:[e.type,` · `,e.transport]})]}),(0,y.jsx)(p,{className:`text-[10px] shrink-0 ${c(r.tone)}`,children:r.label})]})}),(0,y.jsxs)(_,{className:`px-4 pb-4 space-y-2.5`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-1.5 text-xs text-muted-foreground`,children:[(0,y.jsx)(b,{direction:e.direction}),(0,y.jsx)(`span`,{className:`capitalize`,children:e.direction}),e.target&&(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(`span`,{children:`·`}),(0,y.jsx)(`span`,{className:`truncate text-foreground`,children:e.target})]})]}),e.targetHealth&&e.targetHealth.status!==`ok`&&(0,y.jsx)(`div`,{className:`text-xs px-2 py-1 rounded ${e.targetHealth.status===`error`?`bg-red-500/10 text-red-400`:`bg-yellow-500/10 text-yellow-400`}`,children:e.targetHealth.detail}),l(e.capabilities).length>0&&(0,y.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:l(e.capabilities).map(e=>(0,y.jsx)(p,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))}),e.tags.length>0&&(0,y.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:e.tags.map(e=>(0,y.jsx)(p,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))}),(0,y.jsxs)(`div`,{className:`flex items-center justify-between pt-0.5`,children:[(0,y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:u(t,e.lastSeen)}),(0,y.jsxs)(m,{size:`sm`,variant:`outline`,className:`h-7 text-xs gap-1`,onClick:()=>n(e),children:[(0,y.jsx)(i,{className:`w-3 h-3`}),`Details`]})]})]})]})}function S(){let e=s(e=>e.channels),t=e.filter(f).length,n=e.filter(e=>e.targetHealth?.status===`error`).length,r=e.filter(e=>e.targetHealth?.status===`warning`).length;return(0,y.jsxs)(`div`,{className:`space-y-4`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`h2`,{className:`text-base font-semibold`,children:`Channels`}),(0,y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Inbound / outbound message integrations`})]}),(0,y.jsxs)(`div`,{className:`flex items-center gap-2 ml-auto`,children:[t>0&&(0,y.jsxs)(p,{className:`text-xs bg-emerald-500/15 text-emerald-400 border-emerald-500/30`,children:[t,` ready`]}),r>0&&(0,y.jsxs)(p,{className:`text-xs bg-yellow-500/15 text-yellow-400 border-yellow-500/30`,children:[r,` warning`]}),n>0&&(0,y.jsxs)(p,{className:`text-xs bg-red-500/15 text-red-400 border-red-500/30`,children:[n,` error`]})]})]}),e.length===0?(0,y.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No channels registered.`}):(0,y.jsx)(`div`,{className:`grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4`,children:e.map(e=>(0,y.jsx)(x,{channel:e},e.id))})]})}export{S as ChannelsView};
|
|
2
|
-
//# sourceMappingURL=channels-BG0Nib7y.js.map
|