agent-relay-server 0.61.2 → 0.62.1

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.
Files changed (181) hide show
  1. package/docs/openapi.json +479 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-DVvlvi4B.js → MessageBubble-DRrxfjyT.js} +2 -2
  4. package/public/assets/{MessageBubble-DVvlvi4B.js.map → MessageBubble-DRrxfjyT.js.map} +1 -1
  5. package/public/assets/PlanGraphPanel-Dp2eYazz.js +2 -0
  6. package/public/assets/PlanGraphPanel-Dp2eYazz.js.map +1 -0
  7. package/public/assets/{activity-IYl3MEXC.js → activity--t0U2yzq.js} +2 -2
  8. package/public/assets/{activity-IYl3MEXC.js.map → activity--t0U2yzq.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BfGTTbeY.js → agent-profiles-BMtJYzxn.js} +2 -2
  10. package/public/assets/{agent-profiles-BfGTTbeY.js.map → agent-profiles-BMtJYzxn.js.map} +1 -1
  11. package/public/assets/{agents-BquvREQr.js → agents-Ck9AePlc.js} +2 -2
  12. package/public/assets/{agents-BquvREQr.js.map → agents-Ck9AePlc.js.map} +1 -1
  13. package/public/assets/{analytics-De0cEZGx.js → analytics-CzfK387L.js} +3 -3
  14. package/public/assets/{analytics-De0cEZGx.js.map → analytics-CzfK387L.js.map} +1 -1
  15. package/public/assets/automation-Cgw8vtE8.js +2 -0
  16. package/public/assets/automation-Cgw8vtE8.js.map +1 -0
  17. package/public/assets/{badge-DhLyCPag.js → badge-CZhIdlTJ.js} +2 -2
  18. package/public/assets/{badge-DhLyCPag.js.map → badge-CZhIdlTJ.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-DSyWKluD.js → branch-state-badge-DnEEPtOP.js} +2 -2
  20. package/public/assets/{branch-state-badge-DSyWKluD.js.map → branch-state-badge-DnEEPtOP.js.map} +1 -1
  21. package/public/assets/{button-K20wAZG1.js → button-2taQeNHW.js} +2 -2
  22. package/public/assets/{button-K20wAZG1.js.map → button-2taQeNHW.js.map} +1 -1
  23. package/public/assets/card-DzIsAI41.js +2 -0
  24. package/public/assets/{card-CcZ9hLI7.js.map → card-DzIsAI41.js.map} +1 -1
  25. package/public/assets/channels-Dc3L2MQs.js +2 -0
  26. package/public/assets/{channels-BG0Nib7y.js.map → channels-Dc3L2MQs.js.map} +1 -1
  27. package/public/assets/chat-Fc6iJ7sa.js +2 -0
  28. package/public/assets/{chat-Dx6qWNxb.js.map → chat-Fc6iJ7sa.js.map} +1 -1
  29. package/public/assets/{connectors-DWh7MwV_.js → connectors-Dc2ttGWs.js} +2 -2
  30. package/public/assets/{connectors-DWh7MwV_.js.map → connectors-Dc2ttGWs.js.map} +1 -1
  31. package/public/assets/copy-button-HEkwi6xX.js +2 -0
  32. package/public/assets/{copy-button-eF3cKp4Q.js.map → copy-button-HEkwi6xX.js.map} +1 -1
  33. package/public/assets/display-DzNvOu1j.js.map +1 -1
  34. package/public/assets/dist-1rXN5FN9.js +2 -0
  35. package/public/assets/{dist-YpHfnnIA.js.map → dist-1rXN5FN9.js.map} +1 -1
  36. package/public/assets/{dist-1dhfdhm9.js → dist-AbEQuFoK.js} +2 -2
  37. package/public/assets/{dist-1dhfdhm9.js.map → dist-AbEQuFoK.js.map} +1 -1
  38. package/public/assets/{dist-COQuunEv.js → dist-B7p1zkvf.js} +2 -2
  39. package/public/assets/{dist-COQuunEv.js.map → dist-B7p1zkvf.js.map} +1 -1
  40. package/public/assets/{dist-Ci5Y37nj.js → dist-CG10hM1-.js} +2 -2
  41. package/public/assets/{dist-Ci5Y37nj.js.map → dist-CG10hM1-.js.map} +1 -1
  42. package/public/assets/dist-Cfm5_zPB.js +2 -0
  43. package/public/assets/{dist-qd198ib9.js.map → dist-Cfm5_zPB.js.map} +1 -1
  44. package/public/assets/{dist-BrQsqBbc.js → dist-Cgw85UcE.js} +2 -2
  45. package/public/assets/{dist-BrQsqBbc.js.map → dist-Cgw85UcE.js.map} +1 -1
  46. package/public/assets/{dist-CoROafNg.js → dist-D3_TJKTC.js} +2 -2
  47. package/public/assets/{dist-CoROafNg.js.map → dist-D3_TJKTC.js.map} +1 -1
  48. package/public/assets/{dist-I-MFUv51.js → dist-DQkLq0hs.js} +2 -2
  49. package/public/assets/{dist-I-MFUv51.js.map → dist-DQkLq0hs.js.map} +1 -1
  50. package/public/assets/{dist-UQGKwqZH.js → dist-D_sl7u_f.js} +2 -2
  51. package/public/assets/{dist-UQGKwqZH.js.map → dist-D_sl7u_f.js.map} +1 -1
  52. package/public/assets/{dist-yGOq6sB3.js → dist-Ilb3r1im.js} +2 -2
  53. package/public/assets/{dist-yGOq6sB3.js.map → dist-Ilb3r1im.js.map} +1 -1
  54. package/public/assets/dist-Pf_3Gf38.js +2 -0
  55. package/public/assets/{dist-BLBF7qqr.js.map → dist-Pf_3Gf38.js.map} +1 -1
  56. package/public/assets/{es2015-CRxz9lOj.js → es2015-GSepNaIS.js} +2 -2
  57. package/public/assets/{es2015-CRxz9lOj.js.map → es2015-GSepNaIS.js.map} +1 -1
  58. package/public/assets/{formatted-body-impl-DMm6jeqi.js → formatted-body-impl-Bg-gx7rG.js} +2 -2
  59. package/public/assets/{formatted-body-impl-DMm6jeqi.js.map → formatted-body-impl-Bg-gx7rG.js.map} +1 -1
  60. package/public/assets/index-BeDncjN4.js +22 -0
  61. package/public/assets/index-BeDncjN4.js.map +1 -0
  62. package/public/assets/index-DiR-fqy_.css +2 -0
  63. package/public/assets/{input-BHYNpXAG.js → input-BsNqlONJ.js} +2 -2
  64. package/public/assets/{input-BHYNpXAG.js.map → input-BsNqlONJ.js.map} +1 -1
  65. package/public/assets/{insights-C7wKt1_l.js → insights-c08aW-7n.js} +2 -2
  66. package/public/assets/{insights-C7wKt1_l.js.map → insights-c08aW-7n.js.map} +1 -1
  67. package/public/assets/{integrations-CG9qyHUf.js → integrations-BwpgcqnF.js} +2 -2
  68. package/public/assets/{integrations-CG9qyHUf.js.map → integrations-BwpgcqnF.js.map} +1 -1
  69. package/public/assets/{lib-Df4aph5u.js → lib-bXPu6xCL.js} +2 -2
  70. package/public/assets/{lib-Df4aph5u.js.map → lib-bXPu6xCL.js.map} +1 -1
  71. package/public/assets/lucide-react-DWYjU-AF.js +9 -0
  72. package/public/assets/{lucide-react-L2UxFDic.js.map → lucide-react-DWYjU-AF.js.map} +1 -1
  73. package/public/assets/{maintenance-CjpvnV_U.js → maintenance-CTKP0kWB.js} +2 -2
  74. package/public/assets/{maintenance-CjpvnV_U.js.map → maintenance-CTKP0kWB.js.map} +1 -1
  75. package/public/assets/{managed-agents-CdfuBKJA.js → managed-agents-BofCxo8d.js} +2 -2
  76. package/public/assets/{managed-agents-CdfuBKJA.js.map → managed-agents-BofCxo8d.js.map} +1 -1
  77. package/public/assets/{markdown-preview-impl-Cf4xCmWt.js → markdown-preview-impl-CQ-6BIb8.js} +2 -2
  78. package/public/assets/{markdown-preview-impl-Cf4xCmWt.js.map → markdown-preview-impl-CQ-6BIb8.js.map} +1 -1
  79. package/public/assets/memory-DvfXjMoi.js +2 -0
  80. package/public/assets/{memory-xKWduhyr.js.map → memory-DvfXjMoi.js.map} +1 -1
  81. package/public/assets/{messages-BpZYOUtJ.js → messages-C9VsPzZ_.js} +2 -2
  82. package/public/assets/{messages-BpZYOUtJ.js.map → messages-C9VsPzZ_.js.map} +1 -1
  83. package/public/assets/orchestrators-MffVueeE.js +2 -0
  84. package/public/assets/{orchestrators-BrJvGIOc.js.map → orchestrators-MffVueeE.js.map} +1 -1
  85. package/public/assets/{overview-BUORwePi.js → overview-BSVajefM.js} +2 -2
  86. package/public/assets/{overview-BUORwePi.js.map → overview-BSVajefM.js.map} +1 -1
  87. package/public/assets/pairs-B5711-Cs.js +2 -0
  88. package/public/assets/{pairs-DR0GizDN.js.map → pairs-B5711-Cs.js.map} +1 -1
  89. package/public/assets/{react-dom-CrP_AtcL.js → react-dom-CPghDLpB.js} +2 -2
  90. package/public/assets/{react-dom-CrP_AtcL.js.map → react-dom-CPghDLpB.js.map} +1 -1
  91. package/public/assets/{security-DLwSqTc_.js → security-DkUwK9Vj.js} +2 -2
  92. package/public/assets/{security-DLwSqTc_.js.map → security-DkUwK9Vj.js.map} +1 -1
  93. package/public/assets/{select-pEru111r.js → select-DrM2RRiT.js} +2 -2
  94. package/public/assets/{select-pEru111r.js.map → select-DrM2RRiT.js.map} +1 -1
  95. package/public/assets/{settings-B-ueGvwV.js → settings-DdKCb-vy.js} +3 -3
  96. package/public/assets/{settings-B-ueGvwV.js.map → settings-DdKCb-vy.js.map} +1 -1
  97. package/public/assets/store-iTiheuTD.js +9 -0
  98. package/public/assets/store-iTiheuTD.js.map +1 -0
  99. package/public/assets/tasks-e1y1PFfO.js +2 -0
  100. package/public/assets/{tasks-C2rRQm6E.js.map → tasks-e1y1PFfO.js.map} +1 -1
  101. package/public/assets/teams-BUcxoGC_.js +2 -0
  102. package/public/assets/teams-BUcxoGC_.js.map +1 -0
  103. package/public/assets/{terminal-viewer-impl-BM_3qoeN.js → terminal-viewer-impl-CmZjQOB2.js} +3 -3
  104. package/public/assets/{terminal-viewer-impl-BM_3qoeN.js.map → terminal-viewer-impl-CmZjQOB2.js.map} +1 -1
  105. package/public/assets/{use-keyboard-viewport-DrRH_QzG.js → use-keyboard-viewport-NKw1O5gC.js} +2 -2
  106. package/public/assets/{use-keyboard-viewport-DrRH_QzG.js.map → use-keyboard-viewport-NKw1O5gC.js.map} +1 -1
  107. package/public/assets/{work-queue-CkIYDAZD.js → work-queue-CXBQx43n.js} +2 -2
  108. package/public/assets/{work-queue-CkIYDAZD.js.map → work-queue-CXBQx43n.js.map} +1 -1
  109. package/public/assets/{workspaces-CYm5bLg9.js → workspaces-D2EfZh9a.js} +3 -3
  110. package/public/assets/{workspaces-CYm5bLg9.js.map → workspaces-D2EfZh9a.js.map} +1 -1
  111. package/public/index.html +21 -21
  112. package/src/automations.ts +40 -7
  113. package/src/branch-landed.ts +5 -0
  114. package/src/bus.ts +6 -1
  115. package/src/cli/workspace.ts +9 -17
  116. package/src/db/activity.ts +191 -3
  117. package/src/db/agents.ts +14 -5
  118. package/src/db/integrations.ts +1 -0
  119. package/src/db/issue-associations.ts +128 -0
  120. package/src/db/issue-lifecycle.ts +187 -0
  121. package/src/db/issue-refs.ts +254 -0
  122. package/src/db/mappers.ts +2 -0
  123. package/src/db/migrations.ts +53 -0
  124. package/src/db/schema.ts +3 -2
  125. package/src/issue-tracker/adapter.ts +24 -0
  126. package/src/issue-tracker/github.ts +136 -0
  127. package/src/issue-tracker/index.ts +13 -0
  128. package/src/issue-tracker/registry.ts +26 -0
  129. package/src/maintenance.ts +22 -169
  130. package/src/mcp-issue-activity-tools.ts +114 -0
  131. package/src/mcp-issue-association-tools.ts +130 -0
  132. package/src/mcp-issue-lifecycle-tools.ts +55 -0
  133. package/src/mcp-issue-ref-tools.ts +174 -0
  134. package/src/mcp-team-tools.ts +1 -1
  135. package/src/mcp.ts +3 -3
  136. package/src/provider-config-store.ts +30 -8
  137. package/src/routes/activity.ts +6 -1
  138. package/src/routes/commands.ts +29 -0
  139. package/src/routes/index.ts +19 -0
  140. package/src/routes/issue-activity.ts +78 -0
  141. package/src/routes/issue-associations.ts +76 -0
  142. package/src/routes/issue-lifecycle.ts +34 -0
  143. package/src/routes/issue-refs.ts +137 -0
  144. package/src/routes/orchestrator-bootstrap.ts +2 -1
  145. package/src/routes/orchestrator.ts +42 -1
  146. package/src/routes/tokens.ts +1 -1
  147. package/src/runtime-tokens.ts +15 -11
  148. package/src/security.ts +51 -3
  149. package/src/services/issue-activity.ts +30 -0
  150. package/src/services/issue-associations.ts +72 -0
  151. package/src/services/issue-lifecycle.ts +308 -0
  152. package/src/services/issue-refs.ts +32 -0
  153. package/src/services/plan-graph.ts +40 -2
  154. package/src/services/team.ts +4 -0
  155. package/src/token-db.ts +27 -7
  156. package/src/workspace-actions.ts +8 -1
  157. package/src/workspace-auto-merge.ts +228 -0
  158. package/src/workspace-probe.ts +125 -0
  159. package/public/assets/PlanGraphPanel-Dby7wq9c.js +0 -2
  160. package/public/assets/PlanGraphPanel-Dby7wq9c.js.map +0 -1
  161. package/public/assets/automation-Ddbermbi.js +0 -2
  162. package/public/assets/automation-Ddbermbi.js.map +0 -1
  163. package/public/assets/card-CcZ9hLI7.js +0 -2
  164. package/public/assets/channels-BG0Nib7y.js +0 -2
  165. package/public/assets/chat-Dx6qWNxb.js +0 -2
  166. package/public/assets/copy-button-eF3cKp4Q.js +0 -2
  167. package/public/assets/dist-BLBF7qqr.js +0 -2
  168. package/public/assets/dist-YpHfnnIA.js +0 -2
  169. package/public/assets/dist-qd198ib9.js +0 -2
  170. package/public/assets/index-B3wOeAPO.js +0 -21
  171. package/public/assets/index-B3wOeAPO.js.map +0 -1
  172. package/public/assets/index-Dn_GDXbi.css +0 -2
  173. package/public/assets/lucide-react-L2UxFDic.js +0 -9
  174. package/public/assets/memory-xKWduhyr.js +0 -2
  175. package/public/assets/orchestrators-BrJvGIOc.js +0 -2
  176. package/public/assets/pairs-DR0GizDN.js +0 -2
  177. package/public/assets/store-B8xP6z6T.js +0 -9
  178. package/public/assets/store-B8xP6z6T.js.map +0 -1
  179. package/public/assets/tasks-C2rRQm6E.js +0 -2
  180. package/public/assets/teams-Bo0sdBTp.js +0 -2
  181. package/public/assets/teams-Bo0sdBTp.js.map +0 -1
@@ -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{r as e}from"./chunk-CilyBKbf.js";import{B as t,In as n,Jn as r,L as i,Mt as a,O as o,hn as s,k as c,m as l,z as u}from"./lucide-react-L2UxFDic.js";import{i as d,o as f,t as p}from"./store-B8xP6z6T.js";import{U as m,h}from"./display-DzNvOu1j.js";import{t as g}from"./badge-DhLyCPag.js";import{t as _}from"./button-K20wAZG1.js";import{t as v}from"./input-BHYNpXAG.js";import{N as y,f as b,n as x,r as S}from"./index-B3wOeAPO.js";import{a as C,o as w,r as T,t as E}from"./card-CcZ9hLI7.js";var D=e(r(),1),O=n(),k={scheduled:`bg-blue-500/10 text-blue-400 border-blue-500/20`,dispatching:`bg-sky-500/10 text-sky-400 border-sky-500/20`,waiting_agent:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,running:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,succeeded:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,failed:`bg-red-500/10 text-red-400 border-red-500/20`,canceled:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,timed_out:`bg-red-500/10 text-red-400 border-red-500/20`},A=new Set([`scheduled`,`dispatching`,`waiting_agent`,`running`]);function j(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`budget elapsed`:e.status}function M(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`:k[e.status]||``}function N(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?void 0:e.error}function P(e){return A.has(e.status)}function F(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function I(e){return typeof e==`string`&&e.length>0?e:void 0}function L(e){let t=Math.max(0,Math.round(e/1e3)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60;return n?`${n}h ${r}m`:r?`${r}m ${i}s`:`${i}s`}function R(e){return F(e.meta?.runtimeBudgetDeadlineAt)??F(e.result?.deadlineAt)}function z(e,t){if(!e.startedAt)return`-`;let n=e.finishedAt||(P(e)?t:e.updatedAt);return L(Number(n)-Number(e.startedAt))}function B(e,t){let n=R(e);if(!(!n||!P(e)))return L(n-t)}function V(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`runtime budget elapsed`:e.error}function H(e,t,n){let r=e.targetAgentId||e.spawnedAgentId||I(e.meta?.runtimeAgentId),i=r?t.find(e=>e.id===r):void 0,a=r?n.flatMap(e=>e.managedAgents.map(t=>({orchestrator:e,managedAgent:t}))).find(({managedAgent:e})=>e.agentId===r):void 0,o=I(e.meta?.terminalSession)||I(e.meta?.tmuxSession)||I(e.meta?.sessionName)||I(i?.meta?.terminalSession)||I(i?.meta?.tmuxSession)||I(i?.meta?.sessionName)||a?.managedAgent.terminalSession||a?.managedAgent.tmuxSession||a?.managedAgent.sessionName,s=I(e.meta?.orchestratorId)||a?.orchestrator.id||e.orchestratorId;return o?{orchestratorId:s,session:o}:void 0}function U(e=``){return{name:``,description:``,enabled:!0,schedule:`0 8 * * *`,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`,orchestratorId:e,targetMode:`existing_agent`,provider:`codex`,model:d.codex.defaultModel||``,effort:``,profile:`default-relay`,label:``,tags:``,capabilities:``,ifNoMatch:`fail`,cwd:``,approvalMode:`guarded`,workspaceMode:`inherit`,keepAlive:!1,budgetMinutes:``,warningMinutes:``,warningMessage:``,concurrencyPolicy:`skip`,title:``,body:``,severity:`info`,dedupeKey:``,externalUrl:``}}function W(e){let t=e.split(`,`).map(e=>e.trim()).filter(Boolean);return t.length?t:void 0}function G(e){let t=e.targetPolicy;return{...U(e.orchestratorId),id:e.id,name:e.name,description:e.description||``,enabled:e.enabled,schedule:e.schedule,timezone:e.timezone,orchestratorId:e.orchestratorId,targetMode:t.mode,provider:t.mode===`on_demand_agent`?t.provider:t.selector.provider||`codex`,model:t.mode===`on_demand_agent`&&(t.model||d[t.provider].defaultModel)||``,effort:t.mode===`on_demand_agent`&&t.effort||``,profile:t.mode===`on_demand_agent`&&t.profile||`default-relay`,label:t.mode===`existing_agent`&&t.selector.label||``,tags:t.mode===`existing_agent`?(t.selector.tags||[]).join(`, `):``,capabilities:t.mode===`existing_agent`?(t.selector.capabilities||[]).join(`, `):``,ifNoMatch:t.mode===`existing_agent`&&t.ifNoMatch||`fail`,cwd:t.mode===`on_demand_agent`&&t.cwd||``,approvalMode:t.mode===`on_demand_agent`&&t.approvalMode||`guarded`,workspaceMode:t.mode===`on_demand_agent`&&t.workspaceMode||`inherit`,keepAlive:t.mode===`on_demand_agent`?!!t.keepAlive:!1,budgetMinutes:t.mode===`on_demand_agent`&&t.runtimeBudget?String(Math.round(t.runtimeBudget.maxRuntimeMs/6e4)):``,warningMinutes:t.mode===`on_demand_agent`&&t.runtimeBudget?.warnAtMs!==void 0?String(Math.round(t.runtimeBudget.warnAtMs/6e4)):``,warningMessage:t.mode===`on_demand_agent`&&t.runtimeBudget?.warningMessage||``,concurrencyPolicy:e.concurrencyPolicy,title:e.taskTemplate.title,body:e.taskTemplate.body,severity:e.taskTemplate.severity||`info`,dedupeKey:e.taskTemplate.dedupeKey||``,externalUrl:e.taskTemplate.externalUrl||``}}function K(e){let t=Number(e.budgetMinutes),n=Number(e.warningMinutes),r=e.targetMode===`on_demand_agent`&&e.budgetMinutes.trim()&&Number.isFinite(t)?{maxRuntimeMs:Math.round(t*6e4),warnAtMs:e.warningMinutes.trim()&&Number.isFinite(n)?Math.round(n*6e4):void 0,warningMessage:e.warningMessage.trim()||void 0}:void 0,i=e.targetMode===`existing_agent`?{mode:`existing_agent`,selector:{provider:e.provider||void 0,label:e.label.trim()||void 0,tags:W(e.tags),capabilities:W(e.capabilities)},ifNoMatch:e.ifNoMatch}:{mode:`on_demand_agent`,provider:e.provider,model:e.model||void 0,effort:e.effort?e.effort:void 0,profile:e.profile||void 0,cwd:e.cwd.trim()||void 0,approvalMode:e.approvalMode,workspaceMode:e.workspaceMode,keepAlive:e.keepAlive,runtimeBudget:r};return{kind:`scheduled_task`,name:e.name.trim(),description:e.description.trim()||void 0,enabled:e.enabled,schedule:e.schedule.trim(),timezone:e.timezone.trim()||`UTC`,catchUpPolicy:`skip`,concurrencyPolicy:e.concurrencyPolicy,orchestratorId:e.orchestratorId,targetPolicy:i,taskTemplate:{title:e.title.trim(),body:e.body.trim(),severity:e.severity,dedupeKey:e.dedupeKey.trim()||void 0,externalUrl:e.externalUrl.trim()||void 0}}}function q(){let e=f(),t=p(e=>e.automations),n=p(e=>e.automationRuns),r=p(e=>e.orchestrators),a=p(e=>e.agents),o=p(e=>e.apiCall),c=p(e=>e.showError),l=p(e=>e.openConfirm),u=p(e=>e.fetchAutomations),d=p(e=>e.fetchAutomationRuns),m=p(e=>e.fetchTasks),h=p(e=>e.agentProfiles),[v,y]=(0,D.useState)(``),[b,x]=(0,D.useState)(()=>U(r[0]?.id||``)),[S,C]=(0,D.useState)(!1),[w,T]=(0,D.useState)(null),E=t.find(e=>e.id===v),k=r.filter(e=>e.status===`online`),A=n.filter(P);(0,D.useEffect)(()=>{Promise.all([u(),d()])},[]),(0,D.useEffect)(()=>{!b.orchestratorId&&r[0]&&x(e=>({...e,orchestratorId:r[0].id}))},[r]);let j=(0,D.useMemo)(()=>{let e=new Map;for(let t of n){let n=e.get(t.automationId)||[];n.push(t),e.set(t.automationId,n)}return e},[n]);function M(){y(``),x(U(r[0]?.id||``))}function N(e){y(e.id),x(G(e))}async function F(){C(!0);try{let e=K(b);if(b.id)await o(`PATCH`,`/automations/`+encodeURIComponent(b.id),e);else{let t=await o(`POST`,`/automations`,e);y(t.id),x(G(t))}await Promise.all([u(),d()])}catch(e){c(`Save Failed`,e.message)}finally{C(!1)}}async function I(e,t){try{await o(`PATCH`,`/automations/`+encodeURIComponent(e.id),t),await u()}catch(e){c(`Update Failed`,e.message)}}async function L(e){try{await o(`POST`,`/automations/`+encodeURIComponent(e.id)+`/run`),await Promise.all([d(),m()])}catch(e){c(`Run Failed`,e.message)}}function R(e){l(`Delete Automation`,`Delete automation "${e.name}"?`,async()=>{try{await o(`DELETE`,`/automations/`+encodeURIComponent(e.id)),v===e.id&&M(),await Promise.all([u(),d()])}catch(e){c(`Delete Failed`,e.message)}})}return(0,O.jsxs)(`div`,{className:`space-y-4`,children:[(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-3`,children:[(0,O.jsx)(s,{className:`w-5 h-5 text-muted-foreground`}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Automation`}),(0,O.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Scheduled tasks`})]}),(0,O.jsxs)(`div`,{className:`ml-auto flex flex-wrap items-center gap-2`,children:[(0,O.jsxs)(g,{variant:`outline`,className:`text-xs`,children:[t.length,` schedules`]}),(0,O.jsxs)(g,{variant:`outline`,className:`text-xs`,children:[A.length,` running`]}),(0,O.jsxs)(_,{size:`sm`,className:`gap-1`,onClick:M,children:[(0,O.jsx)(i,{className:`w-3.5 h-3.5`}),`New`]})]})]}),A.length>0&&(0,O.jsx)(J,{runs:A,automations:t,agents:a,orchestrators:r,now:e,logRunId:w,onToggleLogs:e=>T(t=>t===e?null:e)}),(0,O.jsxs)(`div`,{className:`grid gap-4 xl:grid-cols-[minmax(0,1fr)_420px]`,children:[(0,O.jsx)(`div`,{className:`space-y-3`,children:t.length===0?(0,O.jsx)(`div`,{className:`py-16 text-center text-sm text-muted-foreground`,children:`No automations`}):t.map(t=>(0,O.jsx)(Y,{automation:t,runs:j.get(t.id)||[],now:e,selected:v===t.id,onEdit:()=>N(t),onRun:()=>L(t),onToggle:()=>I(t,{enabled:!t.enabled}),onDelete:()=>R(t)},t.id))}),(0,O.jsx)(X,{form:b,agentProfiles:h,selected:E,saving:S,orchestrators:k.length?k:r,onChange:e=>x(t=>({...t,...e})),onSave:F})]}),(0,O.jsx)(Q,{runs:n,automations:t,agents:a,orchestrators:r,now:e,logRunId:w,onToggleLogs:e=>T(t=>t===e?null:e)})]})}function J({runs:e,automations:t,agents:n,orchestrators:r,now:i,logRunId:o,onToggleLogs:s}){let c=new Map(t.map(e=>[e.id,e.name]));return(0,O.jsxs)(`div`,{className:`rounded-lg border border-border bg-card/40`,children:[(0,O.jsx)(`div`,{className:`border-b border-border px-3 py-2 text-sm font-medium`,children:`Active runs`}),(0,O.jsx)(`div`,{className:`divide-y divide-border`,children:e.map(e=>{let t=H(e,n,r),l=B(e,i);return(0,O.jsxs)(`div`,{className:`space-y-3 px-3 py-3`,children:[(0,O.jsxs)(`div`,{className:`grid gap-3 text-xs md:grid-cols-[minmax(12rem,1fr)_7rem_7rem_7rem_8rem_auto] md:items-center`,children:[(0,O.jsxs)(`div`,{className:`min-w-0`,children:[(0,O.jsx)(`div`,{className:`truncate text-sm font-medium`,children:c.get(e.automationId)||e.automationId.slice(0,12)}),(0,O.jsx)(`div`,{className:`truncate font-mono text-muted-foreground`,children:e.targetAgentId||e.spawnedAgentId||I(e.meta?.onDemandLabel)||`-`})]}),(0,O.jsx)(g,{variant:`outline`,className:`border text-[10px] w-fit ${M(e)}`,children:j(e)}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:`elapsed `}),z(e,i)]}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:`left `}),l||`-`]}),(0,O.jsx)(`div`,{className:`font-mono text-muted-foreground`,children:e.taskId?`task #${e.taskId}`:`-`}),t&&(0,O.jsxs)(_,{type:`button`,size:`sm`,variant:`outline`,className:`h-7 gap-1 text-xs`,onClick:()=>s(e.id),children:[(0,O.jsx)(a,{className:`w-3 h-3`}),`Logs`]})]}),o===e.id&&t&&(0,O.jsx)(b,{orchestratorId:t.orchestratorId,session:t.session})]},e.id)})})]})}function Y({automation:e,runs:n,now:r,selected:i,onEdit:a,onRun:o,onToggle:s,onDelete:c}){let d=n[0],f=e.targetPolicy,p=f.mode===`existing_agent`?[f.selector.provider,f.selector.label?`label:${f.selector.label}`:``,...(f.selector.tags||[]).map(e=>`tag:${e}`),...(f.selector.capabilities||[]).map(e=>`cap:${e}`)].filter(Boolean).join(` `):`${f.provider} on demand`;return(0,O.jsxs)(E,{className:`${i?`ring-1 ring-primary/50`:``} ${e.enabled?``:`opacity-60`}`,children:[(0,O.jsx)(C,{className:`px-4 py-3`,children:(0,O.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,O.jsx)(y,{checked:e.enabled,onCheckedChange:s,className:`mt-0.5`}),(0,O.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,O.jsx)(w,{className:`text-sm truncate`,children:e.name}),(0,O.jsxs)(`div`,{className:`mt-1 flex flex-wrap gap-1.5 text-xs text-muted-foreground`,children:[(0,O.jsx)(`span`,{className:`font-mono`,children:e.schedule}),(0,O.jsx)(`span`,{children:e.timezone}),(0,O.jsx)(`span`,{children:e.orchestratorId}),(0,O.jsx)(`span`,{className:`truncate max-w-[28rem]`,children:p})]})]}),(0,O.jsxs)(`div`,{className:`flex gap-1`,children:[(0,O.jsx)(_,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Run now`,onClick:o,children:(0,O.jsx)(u,{className:`w-3.5 h-3.5`})}),(0,O.jsx)(_,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Edit`,onClick:a,children:(0,O.jsx)(t,{className:`w-3.5 h-3.5`})}),(0,O.jsx)(_,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Delete`,onClick:c,children:(0,O.jsx)(l,{className:`w-3.5 h-3.5`})})]})]})}),(0,O.jsx)(T,{className:`px-4 pb-3 pt-0`,children:(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 text-xs text-muted-foreground`,children:[(0,O.jsxs)(`span`,{title:e.nextRunAt?h(e.nextRunAt):void 0,children:[`next `,e.nextRunAt?m(r,e.nextRunAt):`disabled`]}),d&&(0,O.jsx)(g,{variant:`outline`,className:`border text-[10px] ${M(d)}`,children:j(d)}),d?.taskId&&(0,O.jsxs)(`span`,{children:[`task #`,d.taskId]}),d&&N(d)&&(0,O.jsx)(`span`,{className:`text-red-400 truncate`,children:N(d)})]})})]})}function X({form:e,selected:t,saving:n,orchestrators:r,agentProfiles:i,onChange:a,onSave:s}){let l=r.find(t=>t.id===e.orchestratorId)?.providers||[],u=d[e.provider]?.models||[],f=u.find(t=>t.alias===e.model)?.efforts||[];function p(t){let n=r.find(e=>e.id===t)?.providers[0]||e.provider;a({orchestratorId:t,provider:n,model:d[n]?.defaultModel||``,effort:``})}function m(e){a({provider:e,model:d[e]?.defaultModel||``,effort:``})}return(0,O.jsxs)(`div`,{className:`rounded-lg border border-border bg-card p-4 space-y-4 h-fit xl:sticky xl:top-4`,children:[(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`h3`,{className:`text-sm font-semibold`,children:t?`Edit schedule`:`New schedule`}),(0,O.jsx)(g,{variant:e.enabled?`default`:`secondary`,className:`ml-auto text-[10px]`,children:e.enabled?`enabled`:`disabled`})]}),(0,O.jsxs)(`div`,{className:`grid gap-3`,children:[(0,O.jsx)(Z,{label:`Name`,children:(0,O.jsx)(v,{value:e.name,onChange:e=>a({name:e.target.value})})}),(0,O.jsx)(Z,{label:`Description`,children:(0,O.jsx)(v,{value:e.description,onChange:e=>a({description:e.target.value})})}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Cron`,children:(0,O.jsx)(v,{value:e.schedule,onChange:e=>a({schedule:e.target.value}),className:`font-mono`})}),(0,O.jsx)(Z,{label:`Timezone`,children:(0,O.jsx)(v,{value:e.timezone,onChange:e=>a({timezone:e.target.value})})})]}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Orchestrator`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.orchestratorId,onChange:e=>p(e.target.value),children:[(0,O.jsx)(`option`,{value:``,children:`Select`}),r.map(e=>(0,O.jsxs)(`option`,{value:e.id,children:[e.hostname,` (`,e.id,`)`]},e.id))]})}),(0,O.jsx)(Z,{label:`Concurrency`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.concurrencyPolicy,onChange:e=>a({concurrencyPolicy:e.target.value}),children:[(0,O.jsx)(`option`,{value:`skip`,children:`skip`}),(0,O.jsx)(`option`,{value:`replace`,children:`replace`}),(0,O.jsx)(`option`,{value:`queue`,children:`queue`})]})})]}),(0,O.jsxs)(`label`,{className:`flex items-center justify-between rounded-md border border-border px-3 py-2`,children:[(0,O.jsx)(`span`,{className:`text-sm`,children:`Enabled`}),(0,O.jsx)(y,{checked:e.enabled,onCheckedChange:e=>a({enabled:e})})]})]}),(0,O.jsxs)(`div`,{className:`space-y-3`,children:[(0,O.jsx)(x,{children:`Target`}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-2`,children:[(0,O.jsx)(_,{type:`button`,variant:e.targetMode===`existing_agent`?`default`:`outline`,onClick:()=>a({targetMode:`existing_agent`}),children:`Existing`}),(0,O.jsx)(_,{type:`button`,variant:e.targetMode===`on_demand_agent`?`default`:`outline`,onClick:()=>a({targetMode:`on_demand_agent`}),children:`On demand`})]}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Provider`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.provider,onChange:e=>m(e.target.value),disabled:!l.length,children:[!l.length&&(0,O.jsx)(`option`,{value:e.provider,children:`No providers available`}),l.map(e=>(0,O.jsx)(`option`,{value:e,children:e},e))]})}),e.targetMode===`existing_agent`?(0,O.jsx)(Z,{label:`If no match`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.ifNoMatch,onChange:e=>a({ifNoMatch:e.target.value}),children:[(0,O.jsx)(`option`,{value:`fail`,children:`fail`}),(0,O.jsx)(`option`,{value:`spawn`,children:`spawn`})]})}):(0,O.jsx)(Z,{label:`Approval`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.approvalMode,onChange:e=>a({approvalMode:e.target.value}),children:[(0,O.jsx)(`option`,{value:`guarded`,children:`guarded`}),(0,O.jsx)(`option`,{value:`read-only`,children:`read-only`}),(0,O.jsx)(`option`,{value:`open`,children:`open`})]})})]}),e.targetMode===`on_demand_agent`&&(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Model`,children:(0,O.jsx)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.model,onChange:e=>a({model:e.target.value,effort:``}),children:u.map(e=>(0,O.jsx)(`option`,{value:e.alias,children:e.label},e.alias))})}),(0,O.jsx)(Z,{label:`Effort`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.effort,onChange:e=>a({effort:e.target.value}),disabled:!f.length,children:[(0,O.jsx)(`option`,{value:``,children:`Default`}),f.map(e=>(0,O.jsx)(`option`,{value:e,children:e},e))]})}),(0,O.jsx)(Z,{label:`Agent profile`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.profile||`default-relay`,onChange:e=>a({profile:e.target.value}),children:[!i.length&&(0,O.jsx)(`option`,{value:`default-relay`,children:`default-relay`}),i.map(e=>(0,O.jsx)(`option`,{value:e.name,children:e.name},e.name))]})}),(0,O.jsx)(Z,{label:`Workspace`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.workspaceMode,onChange:e=>a({workspaceMode:e.target.value}),children:[(0,O.jsx)(`option`,{value:`inherit`,children:`inherit (default → isolated worktree)`}),(0,O.jsx)(`option`,{value:`shared`,children:`shared (run in the repo, commit to its branch)`}),(0,O.jsx)(`option`,{value:`isolated`,children:`isolated (fresh git worktree per run)`})]})})]}),e.targetMode===`existing_agent`?(0,O.jsxs)(`div`,{className:`grid gap-3`,children:[(0,O.jsx)(Z,{label:`Label`,children:(0,O.jsx)(v,{value:e.label,onChange:e=>a({label:e.target.value})})}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Tags`,children:(0,O.jsx)(v,{value:e.tags,onChange:e=>a({tags:e.target.value})})}),(0,O.jsx)(Z,{label:`Capabilities`,children:(0,O.jsx)(v,{value:e.capabilities,onChange:e=>a({capabilities:e.target.value})})})]})]}):(0,O.jsxs)(`div`,{className:`grid gap-3`,children:[(0,O.jsx)(Z,{label:`Cwd`,children:(0,O.jsx)(v,{value:e.cwd,onChange:e=>a({cwd:e.target.value})})}),(0,O.jsxs)(`label`,{className:`flex items-center justify-between rounded-md border border-border px-3 py-2`,children:[(0,O.jsx)(`span`,{className:`text-sm`,children:`Keep alive`}),(0,O.jsx)(y,{checked:e.keepAlive,onCheckedChange:e=>a({keepAlive:e})})]}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Budget minutes`,children:(0,O.jsx)(v,{type:`number`,min:1,step:1,value:e.budgetMinutes,onChange:e=>a({budgetMinutes:e.target.value})})}),(0,O.jsx)(Z,{label:`Warning minute`,children:(0,O.jsx)(v,{type:`number`,min:0,step:1,value:e.warningMinutes,onChange:e=>a({warningMinutes:e.target.value})})})]}),(0,O.jsx)(Z,{label:`Warning message`,children:(0,O.jsx)(S,{value:e.warningMessage,onChange:e=>a({warningMessage:e.target.value}),className:`min-h-20`})})]})]}),(0,O.jsxs)(`div`,{className:`space-y-3`,children:[(0,O.jsx)(x,{children:`Task`}),(0,O.jsx)(Z,{label:`Title`,children:(0,O.jsx)(v,{value:e.title,onChange:e=>a({title:e.target.value})})}),(0,O.jsx)(Z,{label:`Body`,children:(0,O.jsx)(S,{value:e.body,onChange:e=>a({body:e.target.value}),className:`min-h-28`})}),(0,O.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,O.jsx)(Z,{label:`Severity`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.severity,onChange:e=>a({severity:e.target.value}),children:[(0,O.jsx)(`option`,{value:`info`,children:`info`}),(0,O.jsx)(`option`,{value:`warning`,children:`warning`}),(0,O.jsx)(`option`,{value:`critical`,children:`critical`})]})}),(0,O.jsx)(Z,{label:`Dedupe key`,children:(0,O.jsx)(v,{value:e.dedupeKey,onChange:e=>a({dedupeKey:e.target.value})})}),(0,O.jsx)(Z,{label:`External URL`,children:(0,O.jsx)(v,{value:e.externalUrl,onChange:e=>a({externalUrl:e.target.value})})})]})]}),(0,O.jsxs)(_,{className:`w-full gap-2`,onClick:s,disabled:n||!e.name||!e.schedule||!e.orchestratorId||!e.title||!e.body,children:[n?(0,O.jsx)(c,{className:`w-4 h-4 animate-spin`}):(0,O.jsx)(o,{className:`w-4 h-4`}),`Save`]})]})}function Z({label:e,children:t}){return(0,O.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,O.jsx)(x,{className:`text-xs text-muted-foreground`,children:e}),t]})}function Q({runs:e,automations:t,agents:n,orchestrators:r,now:i,logRunId:o,onToggleLogs:s}){let c=new Map(t.map(e=>[e.id,e.name]));return(0,O.jsxs)(`div`,{className:`space-y-2`,children:[(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`h3`,{className:`text-sm font-semibold`,children:`Runs`}),(0,O.jsx)(g,{variant:`secondary`,className:`text-[10px]`,children:e.length})]}),(0,O.jsxs)(`div`,{className:`rounded-lg border border-border overflow-x-auto`,children:[(0,O.jsxs)(`div`,{className:`grid grid-cols-[minmax(10rem,1fr)_8rem_8rem_7rem_7rem_5rem] gap-3 bg-muted/30 px-3 py-2 text-xs text-muted-foreground`,children:[(0,O.jsx)(`span`,{children:`Automation`}),(0,O.jsx)(`span`,{children:`Status`}),(0,O.jsx)(`span`,{children:`Agent`}),(0,O.jsx)(`span`,{children:`Runtime`}),(0,O.jsx)(`span`,{children:`Updated`}),(0,O.jsx)(`span`,{children:`Logs`})]}),e.length===0?(0,O.jsx)(`div`,{className:`px-3 py-10 text-center text-sm text-muted-foreground`,children:`No runs`}):e.slice(0,80).map(e=>{let t=H(e,n,r),l=B(e,i),u=V(e);return(0,O.jsxs)(`div`,{className:`border-t border-border`,children:[(0,O.jsxs)(`div`,{className:`grid grid-cols-[minmax(10rem,1fr)_8rem_8rem_7rem_7rem_5rem] gap-3 px-3 py-2 text-xs items-center`,children:[(0,O.jsxs)(`div`,{className:`min-w-0`,children:[(0,O.jsx)(`div`,{className:`truncate`,children:c.get(e.automationId)||e.automationId.slice(0,12)}),(0,O.jsxs)(`div`,{className:`truncate text-muted-foreground`,children:[e.taskId?`task #${e.taskId}`:`no task`,l?` · ${l} left`:``,u?` · ${u}`:``]})]}),(0,O.jsx)(g,{variant:`outline`,className:`border text-[10px] w-fit ${M(e)}`,children:j(e)}),(0,O.jsx)(`span`,{className:`truncate font-mono text-muted-foreground`,children:e.targetAgentId||e.spawnedAgentId||`-`}),(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:z(e,i)}),(0,O.jsx)(`span`,{className:`text-muted-foreground`,title:h(e.updatedAt),children:m(i,e.updatedAt)}),t?(0,O.jsx)(_,{type:`button`,size:`sm`,variant:`ghost`,className:`h-7 w-7 p-0`,title:`Open logs`,onClick:()=>s(e.id),children:(0,O.jsx)(a,{className:`w-3.5 h-3.5`})}):(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:`-`})]}),o===e.id&&t&&(0,O.jsx)(`div`,{className:`border-t border-border px-3 py-3`,children:(0,O.jsx)(b,{orchestratorId:t.orchestratorId,session:t.session})})]},e.id)})]})]})}export{q as AutomationView};
2
- //# sourceMappingURL=automation-Ddbermbi.js.map