agent-relay-server 0.53.0 → 0.55.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.
Files changed (141) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +1 -1
  3. package/public/assets/{PlanGraphPanel-C81um9s6.js → PlanGraphPanel-CQ0CH7e9.js} +2 -2
  4. package/public/assets/{PlanGraphPanel-C81um9s6.js.map → PlanGraphPanel-CQ0CH7e9.js.map} +1 -1
  5. package/public/assets/{activity-DwVgT07D.js → activity-CxnH-3Zt.js} +2 -2
  6. package/public/assets/{activity-DwVgT07D.js.map → activity-CxnH-3Zt.js.map} +1 -1
  7. package/public/assets/{agent-profiles-BZ_QdiHU.js → agent-profiles-JhWQY-pg.js} +2 -2
  8. package/public/assets/{agent-profiles-BZ_QdiHU.js.map → agent-profiles-JhWQY-pg.js.map} +1 -1
  9. package/public/assets/agents-Bn7CXOMZ.js +2 -0
  10. package/public/assets/{agents-DV5y_tF1.js.map → agents-Bn7CXOMZ.js.map} +1 -1
  11. package/public/assets/{analytics-CzmIT4LJ.js → analytics-BCVgYihk.js} +3 -3
  12. package/public/assets/{analytics-CzmIT4LJ.js.map → analytics-BCVgYihk.js.map} +1 -1
  13. package/public/assets/{automation-Bcx2SOdF.js → automation-Cu5J1Yh4.js} +2 -2
  14. package/public/assets/{automation-Bcx2SOdF.js.map → automation-Cu5J1Yh4.js.map} +1 -1
  15. package/public/assets/{badge-BR4bvEsy.js → badge-D6yb-Kia.js} +2 -2
  16. package/public/assets/{badge-BR4bvEsy.js.map → badge-D6yb-Kia.js.map} +1 -1
  17. package/public/assets/branch-state-badge-fOM74RDq.js +2 -0
  18. package/public/assets/{branch-state-badge-CzlsNakd.js.map → branch-state-badge-fOM74RDq.js.map} +1 -1
  19. package/public/assets/{button-BwqJZS4O.js → button-B-w6a1Ng.js} +2 -2
  20. package/public/assets/{button-BwqJZS4O.js.map → button-B-w6a1Ng.js.map} +1 -1
  21. package/public/assets/card-j6bjYxys.js +2 -0
  22. package/public/assets/{card-C7JtSyuM.js.map → card-j6bjYxys.js.map} +1 -1
  23. package/public/assets/{channels-By6jRWVP.js → channels-D4q8I1q-.js} +2 -2
  24. package/public/assets/{channels-By6jRWVP.js.map → channels-D4q8I1q-.js.map} +1 -1
  25. package/public/assets/chat-6QaNdZGN.js +2 -0
  26. package/public/assets/{chat-9KhGPR6T.js.map → chat-6QaNdZGN.js.map} +1 -1
  27. package/public/assets/{connectors-DzhH5WYg.js → connectors-DuWMf8KT.js} +2 -2
  28. package/public/assets/{connectors-DzhH5WYg.js.map → connectors-DuWMf8KT.js.map} +1 -1
  29. package/public/assets/{copy-button-nbDhTE0M.js → copy-button-CJDmQ9UB.js} +2 -2
  30. package/public/assets/{copy-button-nbDhTE0M.js.map → copy-button-CJDmQ9UB.js.map} +1 -1
  31. package/public/assets/{dist-BIAvrITs.js → dist-1iqvQR2q.js} +2 -2
  32. package/public/assets/{dist-BIAvrITs.js.map → dist-1iqvQR2q.js.map} +1 -1
  33. package/public/assets/{dist-MdPDPcyo.js → dist-BNF6RqQf.js} +2 -2
  34. package/public/assets/{dist-MdPDPcyo.js.map → dist-BNF6RqQf.js.map} +1 -1
  35. package/public/assets/{dist-Dvspm7RE.js → dist-Bg5mAaEO.js} +2 -2
  36. package/public/assets/{dist-Dvspm7RE.js.map → dist-Bg5mAaEO.js.map} +1 -1
  37. package/public/assets/{dist-C7UYAwBf.js → dist-BtFzbpiK.js} +2 -2
  38. package/public/assets/{dist-C7UYAwBf.js.map → dist-BtFzbpiK.js.map} +1 -1
  39. package/public/assets/{dist-svixSeDF.js → dist-CLUvHFPE.js} +2 -2
  40. package/public/assets/{dist-svixSeDF.js.map → dist-CLUvHFPE.js.map} +1 -1
  41. package/public/assets/{dist-BiVJ8IWp.js → dist-Cl4PfJEy.js} +2 -2
  42. package/public/assets/{dist-BiVJ8IWp.js.map → dist-Cl4PfJEy.js.map} +1 -1
  43. package/public/assets/dist-D1g0W-kO.js +2 -0
  44. package/public/assets/{dist-BkDVLgYg.js.map → dist-D1g0W-kO.js.map} +1 -1
  45. package/public/assets/{dist-DvLO--_O.js → dist-DLc4vQoq.js} +2 -2
  46. package/public/assets/{dist-DvLO--_O.js.map → dist-DLc4vQoq.js.map} +1 -1
  47. package/public/assets/{dist-CSISzAHD.js → dist-HMkrkMzb.js} +2 -2
  48. package/public/assets/{dist-CSISzAHD.js.map → dist-HMkrkMzb.js.map} +1 -1
  49. package/public/assets/dist-MX7mVzej.js +2 -0
  50. package/public/assets/{dist-D5y1HX8f.js.map → dist-MX7mVzej.js.map} +1 -1
  51. package/public/assets/dist-fe-f7EIl.js +2 -0
  52. package/public/assets/{dist-tX4uIBFU.js.map → dist-fe-f7EIl.js.map} +1 -1
  53. package/public/assets/{es2015-Cu7l1536.js → es2015-4dO99MHc.js} +2 -2
  54. package/public/assets/{es2015-Cu7l1536.js.map → es2015-4dO99MHc.js.map} +1 -1
  55. package/public/assets/{formatted-body-impl-tuH3hn4f.js → formatted-body-impl-CTv-ZFNo.js} +2 -2
  56. package/public/assets/{formatted-body-impl-tuH3hn4f.js.map → formatted-body-impl-CTv-ZFNo.js.map} +1 -1
  57. package/public/assets/index-Cobj8Cqd.js +21 -0
  58. package/public/assets/index-Cobj8Cqd.js.map +1 -0
  59. package/public/assets/index-D7FDcoef.css +2 -0
  60. package/public/assets/{input-YoPXrp72.js → input-EazHb1Bc.js} +2 -2
  61. package/public/assets/{input-YoPXrp72.js.map → input-EazHb1Bc.js.map} +1 -1
  62. package/public/assets/{insights-Cmkftpne.js → insights-Ucwp3dHJ.js} +2 -2
  63. package/public/assets/{insights-Cmkftpne.js.map → insights-Ucwp3dHJ.js.map} +1 -1
  64. package/public/assets/{integrations-zGR2Hmle.js → integrations-eRsF4CjO.js} +2 -2
  65. package/public/assets/{integrations-zGR2Hmle.js.map → integrations-eRsF4CjO.js.map} +1 -1
  66. package/public/assets/{lib-D8ccisdr.js → lib-C7poqE0I.js} +3 -3
  67. package/public/assets/{lib-D8ccisdr.js.map → lib-C7poqE0I.js.map} +1 -1
  68. package/public/assets/{lucide-react-ChOGD4vc.js → lucide-react-Dr0-sUx6.js} +2 -2
  69. package/public/assets/{lucide-react-ChOGD4vc.js.map → lucide-react-Dr0-sUx6.js.map} +1 -1
  70. package/public/assets/{maintenance-Dr0Y-QW1.js → maintenance-BW56fE-T.js} +2 -2
  71. package/public/assets/{maintenance-Dr0Y-QW1.js.map → maintenance-BW56fE-T.js.map} +1 -1
  72. package/public/assets/{managed-agents-Dd_CLfkU.js → managed-agents-BkFAu1Ub.js} +2 -2
  73. package/public/assets/{managed-agents-Dd_CLfkU.js.map → managed-agents-BkFAu1Ub.js.map} +1 -1
  74. package/public/assets/{markdown-preview-impl-CasNfCMM.js → markdown-preview-impl-BT43MQ7E.js} +2 -2
  75. package/public/assets/{markdown-preview-impl-CasNfCMM.js.map → markdown-preview-impl-BT43MQ7E.js.map} +1 -1
  76. package/public/assets/memory-Cw279dr5.js +2 -0
  77. package/public/assets/{memory-FCZUrHDG.js.map → memory-Cw279dr5.js.map} +1 -1
  78. package/public/assets/{messages-BVidQ6qj.js → messages-5InjjgUv.js} +2 -2
  79. package/public/assets/{messages-BVidQ6qj.js.map → messages-5InjjgUv.js.map} +1 -1
  80. package/public/assets/{orchestrators-CDLPID66.js → orchestrators-B78CQxLl.js} +2 -2
  81. package/public/assets/{orchestrators-CDLPID66.js.map → orchestrators-B78CQxLl.js.map} +1 -1
  82. package/public/assets/{overview-CBBOJR6C.js → overview-C9V2X2oo.js} +2 -2
  83. package/public/assets/{overview-CBBOJR6C.js.map → overview-C9V2X2oo.js.map} +1 -1
  84. package/public/assets/{pairs-DT1DGrb8.js → pairs-Cc3tOjXB.js} +2 -2
  85. package/public/assets/{pairs-DT1DGrb8.js.map → pairs-Cc3tOjXB.js.map} +1 -1
  86. package/public/assets/{react-dom-DgWuIw1o.js → react-dom-CQSYEHV9.js} +2 -2
  87. package/public/assets/{react-dom-DgWuIw1o.js.map → react-dom-CQSYEHV9.js.map} +1 -1
  88. package/public/assets/{security-BQgWIgl4.js → security-CpBET4KN.js} +2 -2
  89. package/public/assets/{security-BQgWIgl4.js.map → security-CpBET4KN.js.map} +1 -1
  90. package/public/assets/{select-yVL8li9x.js → select-FNVeUxFZ.js} +2 -2
  91. package/public/assets/{select-yVL8li9x.js.map → select-FNVeUxFZ.js.map} +1 -1
  92. package/public/assets/settings-CPto3TdF.js +10 -0
  93. package/public/assets/settings-CPto3TdF.js.map +1 -0
  94. package/public/assets/{store-BlsqvkSE.js → store-h-egULYA.js} +3 -3
  95. package/public/assets/{store-BlsqvkSE.js.map → store-h-egULYA.js.map} +1 -1
  96. package/public/assets/{tasks-CySWSOjp.js → tasks-BPplMcnM.js} +2 -2
  97. package/public/assets/{tasks-CySWSOjp.js.map → tasks-BPplMcnM.js.map} +1 -1
  98. package/public/assets/teams-CCeuRt7v.js +2 -0
  99. package/public/assets/{teams-DhFfsGOb.js.map → teams-CCeuRt7v.js.map} +1 -1
  100. package/public/assets/{terminal-viewer-impl-BUozTzVL.js → terminal-viewer-impl-DRlbl8Yv.js} +3 -3
  101. package/public/assets/{terminal-viewer-impl-BUozTzVL.js.map → terminal-viewer-impl-DRlbl8Yv.js.map} +1 -1
  102. package/public/assets/{use-keyboard-viewport-BY99X7eI.js → use-keyboard-viewport-Iaz6IT4p.js} +2 -2
  103. package/public/assets/{use-keyboard-viewport-BY99X7eI.js.map → use-keyboard-viewport-Iaz6IT4p.js.map} +1 -1
  104. package/public/assets/{work-queue-CYeuuHjM.js → work-queue-qtjloIIF.js} +2 -2
  105. package/public/assets/{work-queue-CYeuuHjM.js.map → work-queue-qtjloIIF.js.map} +1 -1
  106. package/public/assets/workspaces-BcnSfMmi.js +3 -0
  107. package/public/assets/{workspaces-DrJwKq9X.js.map → workspaces-BcnSfMmi.js.map} +1 -1
  108. package/public/index.html +21 -21
  109. package/src/bus.ts +3 -8
  110. package/src/db/agents.ts +8 -4
  111. package/src/db/claim-sql.ts +28 -0
  112. package/src/db/message-reads.ts +98 -4
  113. package/src/db/stats.ts +2 -1
  114. package/src/db/tasks.ts +2 -1
  115. package/src/maintenance.ts +3 -1
  116. package/src/mcp.ts +2 -0
  117. package/src/provider-state.ts +66 -0
  118. package/src/routes/workspaces.ts +2 -2
  119. package/src/services/rate-limit-resume.ts +96 -0
  120. package/src/services/register-agent.ts +4 -1
  121. package/src/services/send-message.ts +13 -1
  122. package/src/services/spawn-agent.ts +15 -0
  123. package/src/services/transient-agent-reaper.ts +5 -1
  124. package/src/spawn-command.ts +8 -0
  125. package/src/workspace-actions.ts +2 -2
  126. package/src/workspace-merge.ts +4 -1
  127. package/public/assets/agents-DV5y_tF1.js +0 -2
  128. package/public/assets/branch-state-badge-CzlsNakd.js +0 -2
  129. package/public/assets/card-C7JtSyuM.js +0 -2
  130. package/public/assets/chat-9KhGPR6T.js +0 -2
  131. package/public/assets/dist-BkDVLgYg.js +0 -2
  132. package/public/assets/dist-D5y1HX8f.js +0 -2
  133. package/public/assets/dist-tX4uIBFU.js +0 -2
  134. package/public/assets/index-BRi2QbMp.js +0 -21
  135. package/public/assets/index-BRi2QbMp.js.map +0 -1
  136. package/public/assets/index-t-8AiaRW.css +0 -2
  137. package/public/assets/memory-FCZUrHDG.js +0 -2
  138. package/public/assets/settings-C5Mg9eH8.js +0 -10
  139. package/public/assets/settings-C5Mg9eH8.js.map +0 -1
  140. package/public/assets/teams-DhFfsGOb.js +0 -2
  141. package/public/assets/workspaces-DrJwKq9X.js +0 -3
@@ -15,9 +15,11 @@ import {
15
15
  import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
16
  import { matchAgents } from "../agent-ref";
17
17
  import { getAgent } from "./agents.ts";
18
+ import { sendMessageWithResult } from "./messages.ts";
18
19
  import { deleteArtifactLinksForEntity } from "./artifacts.ts";
19
20
  import { getDb, timedQuery } from "./connection.ts";
20
- import { legacyChannelTargets } from "./delivery.ts";
21
+ import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
22
+ import { messageTaskClosed } from "./claim-sql.ts";
21
23
  import { MSG_SELECT, rowToMessage } from "./mappers.ts";
22
24
  import type {
23
25
  AgentCard,
@@ -202,9 +204,13 @@ export function pollMessages(query: PollQuery): Message[] {
202
204
  }
203
205
  conditions.push("delivery_status != 'queued' AND delivery_status NOT IN ('failed', 'dead')");
204
206
 
205
- // Hide active claims held by someone else, but let expired claims surface so
206
- // another matching agent can recover stuck work.
207
- conditions.push(`(claimable = 0 OR claimed_by IS NULL OR claimed_by = ? OR ((claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = m.id AND t.status IN ('done', 'failed', 'canceled'))))`);
207
+ // A claimable message is available to this agent when it is non-claimable, currently held by
208
+ // THIS agent, or unclaimed/expired AND its task is not already closed. The closed-task guard
209
+ // must cover BOTH the null-claim and the expired-claim branch: when a managed agent is
210
+ // pruned/deleted its message claims are released to NULL (see pruneOfflineAgents/deleteAgent),
211
+ // so without it a finished task's message resurrects as fresh work on the next restart (which
212
+ // mints a new agent id, so message_reads no longer dedups it) — the automation re-delivery bug.
213
+ conditions.push(`(claimable = 0 OR claimed_by = ? OR ((claimed_by IS NULL OR claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT ${messageTaskClosed("m.id")}))`);
208
214
  params.push(query.for, Date.now());
209
215
 
210
216
  if (query.sinceId !== undefined) {
@@ -319,6 +325,94 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
319
325
  .map((message) => replyObligationFromMessage(message, agentId));
320
326
  }
321
327
 
328
+ /**
329
+ * Channel-answer linking: a channel agent answers the human OUT-OF-BAND through its bridge — e.g.
330
+ * `agent-relay /message telegram '{"action":"reply",...}'` — which stores an outbound to the channel
331
+ * agent with NO reply_to / thread / conversation linkage (the bridge protocol body is opaque to the
332
+ * relay, so the inbound's channel + conversation never make it onto the answer). Without linkage the
333
+ * obligation never clears, and the Stop hook nags the agent into a redundant `/reply` that routes back
334
+ * through the channel as a second message to the human (the Telegram double-message friction).
335
+ *
336
+ * When the agent posts an unlinked message into a channel agent and has EXACTLY ONE open obligation on
337
+ * that channel AT THAT MOMENT, the conversation is unambiguous: this send is the answer. We snapshot
338
+ * that here, at send time, by returning the obligation id to link the outbound to — a decision a later
339
+ * sibling conversation can never retroactively confuse (the reason query-time coverage is unsafe). When
340
+ * two conversations are open the send is unattributable; we leave it unlinked for an explicit `/reply`,
341
+ * which carries the conversation precisely. Provider-agnostic: keys only on the channel agent target,
342
+ * never on the body.
343
+ *
344
+ * Returns the inbound message id this outbound answers, or null when nothing should be auto-linked.
345
+ */
346
+ export function channelObligationAnsweredBy(
347
+ agentId: string,
348
+ channelAgentId: string,
349
+ ): number | null {
350
+ if (!isChannelAgentId(channelAgentId)) return null;
351
+ const open = listPendingReplyObligations(agentId, 50).filter(
352
+ (obligation) => obligation.kind === "channel.event" && obligation.from === channelAgentId,
353
+ );
354
+ return open.length === 1 ? open[0]!.messageId : null;
355
+ }
356
+
357
+ /**
358
+ * #413 / vent #121 — seed a pending reply obligation for a freshly-registered spawned worker so
359
+ * it cannot strand its verdict. A spawned agent gets its task as the positional spawn prompt, NOT
360
+ * as a relay message, so it has no `message_reads` row → no obligation → the Stop hook (#196/#199)
361
+ * finds nothing to enforce and lets the worker idle with its result undelivered. This recovers the
362
+ * spawn command by its `spawnRequestId` (same lookup as `seedContinuationObjectiveFromSpawnPrompt`),
363
+ * and — only when the spawn carried `expectReply` — posts a message FROM the spawner TO the worker
364
+ * and marks it read, making it an obligation immediately. The Stop hook then refuses to let the
365
+ * worker idle until it `/reply`s, which auto-routes back to the spawner and wakes it.
366
+ *
367
+ * Call only on first registration; idempotent regardless (idempotencyKey dedups a double-fire).
368
+ * Never throws — a failure here must not abort registration. Returns the obligation message id,
369
+ * or null when nothing was seeded.
370
+ */
371
+ export function seedSpawnReplyObligation(
372
+ agentId: string,
373
+ spawnedBy: string | undefined,
374
+ spawnRequestId: string | undefined,
375
+ now = Date.now(),
376
+ ): number | null {
377
+ if (!spawnRequestId || !spawnedBy) return null;
378
+ // The spawner must still be registered to author the obligation message.
379
+ if (!getAgent(spawnedBy)) return null;
380
+ try {
381
+ const row = getDb().query(`
382
+ SELECT params FROM commands
383
+ WHERE type = 'agent.spawn' AND correlation_id = ?
384
+ ORDER BY created_at DESC
385
+ LIMIT 1
386
+ `).get(spawnRequestId) as { params: string } | undefined;
387
+ const params = row ? parseJson<unknown>(row.params, {}) : {};
388
+ if (!isRecord(params) || params.expectReply !== true) return null;
389
+
390
+ const objective = typeof params.prompt === "string" ? params.prompt.trim() : "";
391
+ const contract = `Report-back requested by ${spawnedBy}. When you finish your spawned task, reply here with your result so the coordinator is notified — you should not go idle before delivering it.`;
392
+ const body = objective ? `${contract}\n\nYour task was:\n${objective}` : contract;
393
+
394
+ const { message } = sendMessageWithResult({
395
+ from: spawnedBy,
396
+ to: agentId,
397
+ subject: "Report your result when done",
398
+ body,
399
+ replyExpected: true,
400
+ // `payload.source` is the server-owned "this requires a reply" signal (see messageRequiresReply).
401
+ payload: { source: "spawn-obligation", spawnRequestId },
402
+ idempotencyKey: `spawn-obligation:${spawnRequestId}`,
403
+ occurredAt: now,
404
+ });
405
+ // Pre-read so it is a *pending* obligation immediately: the worker got its task via the
406
+ // positional spawn prompt and may finish in one turn without ever polling its inbox, so an
407
+ // unread message would never become an obligation — the exact gap this closes.
408
+ markRead(message.id, agentId);
409
+ return message.id;
410
+ } catch {
411
+ // Registration must succeed even if obligation seeding fails (e.g. a racing spawner exit).
412
+ return null;
413
+ }
414
+ }
415
+
322
416
  export function markRead(messageId: number, agentId: string): boolean {
323
417
  const exists = getDb().query("SELECT 1 FROM messages WHERE id = ?").get(messageId);
324
418
  if (!exists) return false;
package/src/db/stats.ts CHANGED
@@ -16,6 +16,7 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
16
16
  import { matchAgents } from "../agent-ref";
17
17
  import { listAgents } from "./agents.ts";
18
18
  import { listChannels } from "./channels.ts";
19
+ import { messageTaskClosed } from "./claim-sql.ts";
19
20
  import { getDb } from "./connection.ts";
20
21
  import type {
21
22
  AgentCard,
@@ -245,7 +246,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
245
246
  });
246
247
 
247
248
  const expiredMessageClaims = (getDb()
248
- .query("SELECT COUNT(*) as c FROM messages WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))")
249
+ .query(`SELECT COUNT(*) as c FROM messages WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT ${messageTaskClosed("messages.id")}`)
249
250
  .get(now) as any).c as number;
250
251
  checks.push({
251
252
  name: "expired-message-claims",
package/src/db/tasks.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
16
  import { matchAgents } from "../agent-ref";
17
17
  import { getAgent, settleSingleTargetOnDemandTasks, validateAgentSession } from "./agents.ts";
18
+ import { messageTaskClosed } from "./claim-sql.ts";
18
19
  import { ClaimError, getDb } from "./connection.ts";
19
20
  import { rowToTask, rowToTaskEvent } from "./mappers.ts";
20
21
  import { claimMessageRow } from "./messages.ts";
@@ -169,7 +170,7 @@ export function recordTaskEvent(taskId: number, input: {
169
170
  export function releaseExpiredClaims(now: number = Date.now()): { messageIds: number[]; tasks: Task[] } {
170
171
  return getDb().transaction(() => {
171
172
  settleSingleTargetOnDemandTasks("claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?)", [now], now, "claim-lease-expired");
172
- const releasableMessageClaim = "claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))";
173
+ const releasableMessageClaim = `claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT ${messageTaskClosed("messages.id")}`;
173
174
  const messageRows = getDb()
174
175
  .query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
175
176
  .all(now) as any[];
@@ -63,6 +63,7 @@ import { notifySystemMessage } from "./notify";
63
63
  import { pruneExpiredTokenRecords } from "./token-db";
64
64
  import { dissolveTeamById } from "./services/team";
65
65
  import { reapTransientAgents, resetTransientTrackerForTests } from "./services/transient-agent-reaper";
66
+ import { resumeRateLimitedAgents, resetRateLimitResumeTrackerForTests } from "./services/rate-limit-resume";
66
67
  import type { Command, MaintenanceJob, MaintenanceJobRun } from "./types";
67
68
 
68
69
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -135,7 +136,7 @@ const orphanReapEnabled = () => process.env.AGENT_RELAY_ORPHAN_REAP !== "0";
135
136
  // orchestratorId + session identity -> when we first saw it orphaned (and last reaped).
136
137
  const orphanTracker = new Map<string, { firstOrphanedAt: number; lastReapAt?: number }>();
137
138
 
138
- export function resetOrphanTrackerForTests(): void { orphanTracker.clear(); resetTransientTrackerForTests(); }
139
+ export function resetOrphanTrackerForTests(): void { orphanTracker.clear(); resetTransientTrackerForTests(); resetRateLimitResumeTrackerForTests(); }
139
140
 
140
141
  interface MaintenanceJobDefinition {
141
142
  id: string;
@@ -300,6 +301,7 @@ const definitions: MaintenanceJobDefinition[] = [
300
301
  handler: reapOrphanedSessions,
301
302
  },
302
303
  { id: "transient-agent-reaper", title: "Transient agent reaper", description: "Cleanly shut down transient spawned agents once they are idle and any isolated branch work has landed.", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: reapTransientAgents },
304
+ { id: "rate-limit-resume", title: "Rate-limit resume", description: "Auto-resume agents held on a provider usage/rate-limit stall once their reset window has passed (#286).", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: resumeRateLimitedAgents },
303
305
  {
304
306
  id: "team-reap",
305
307
  title: "Team lifecycle close-out",
package/src/mcp.ts CHANGED
@@ -307,6 +307,7 @@ const TOOLS: ToolDefinition[] = [
307
307
  effort: { type: "string", enum: VALID_EFFORTS },
308
308
  approvalMode: { type: "string", enum: APPROVAL_MODES, description: "Permission posture for the worker. Defaults to YOUR OWN approval mode when omitted (a headless `guarded` worker wedges on the first approval prompt), so a coordinator running `open` spawns workers that can act autonomously. Pass an explicit value to narrow a child (e.g. `read-only` reviewer)." },
309
309
  prompt: { type: "string", description: "Initial task/message delivered to the agent on launch — spawn and hand it its first instruction in one call (no separate follow-up message needed)." },
310
+ expectReply: { type: "boolean", description: "Require the worker to report back: it gets a pending reply obligation to you and CANNOT go idle until it delivers its result (which routes to your inbox and wakes you). Defaults true when you pass a prompt — set false for a true fire-and-forget worker that needn't report (e.g. one that only lands a branch)." },
310
311
  systemPromptAppend: { type: "string" },
311
312
  profile: { type: "string", description: "Agent profile name to apply (env, instructions, permissions, MCP/skills, spawn quota)." },
312
313
  tags: { type: "array", items: { type: "string" } },
@@ -868,6 +869,7 @@ async function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknow
868
869
  policyName: optionalString(args.policyName, "policyName", 120),
869
870
  profile: optionalString(args.profile, "profile", 120),
870
871
  prompt: optionalString(args.prompt, "prompt", 16_000),
872
+ expectReply: typeof args.expectReply === "boolean" ? args.expectReply : undefined,
871
873
  systemPromptAppend: optionalString(args.systemPromptAppend, "systemPromptAppend", 64_000),
872
874
  tags: optionalStringArray(args.tags, "tags"),
873
875
  capabilities: optionalStringArray(args.capabilities, "capabilities"),
@@ -0,0 +1,66 @@
1
+ import { isRecord } from "agent-relay-sdk";
2
+
3
+ // One server-side home for reading an agent's provider runtime state (the
4
+ // `meta.providerState` that both Claude and Codex ride for blocked/approval
5
+ // states — see runner setProviderStatus). Imported by the bus transition audit,
6
+ // the transient-agent reaper (don't reap a held agent), and the rate-limit
7
+ // resume sweep. The dashboard has its own client-side copy in display.ts.
8
+
9
+ export interface ProviderStateView {
10
+ state: string;
11
+ reason?: string;
12
+ label?: string;
13
+ /** Unix ms the provider's limit window resets (rate-limit holds). */
14
+ resetAt?: number;
15
+ /** Unix ms the hold was entered (used for the resume fallback when resetAt is absent). */
16
+ enteredAt?: number;
17
+ /** Raw provider error_type, kept for empirical observation of which type a real limit fires. */
18
+ errorType?: string;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ interface HasMeta {
23
+ meta?: Record<string, unknown> | null;
24
+ }
25
+
26
+ /** The agent's provider runtime state, or null when none is set / malformed. */
27
+ export function agentProviderState(agent: HasMeta | null | undefined): ProviderStateView | null {
28
+ const value = agent?.meta?.providerState;
29
+ if (!isRecord(value) || typeof value.state !== "string") return null;
30
+ return value as ProviderStateView;
31
+ }
32
+
33
+ /** True when the agent is in any blocked provider state (rate-limit, approval, model-unavailable). */
34
+ export function isProviderBlocked(agent: HasMeta | null | undefined): boolean {
35
+ return agentProviderState(agent)?.state === "blocked";
36
+ }
37
+
38
+ /** The `reason` value a rate-limit hold carries (matches the runner's control-server seam). */
39
+ export const RATE_LIMIT_BLOCK_REASON = "rate_limit";
40
+
41
+ /** True when the agent is specifically held on a provider usage/rate limit. */
42
+ export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
43
+ const state = agentProviderState(agent);
44
+ return state?.state === "blocked" && state.reason === RATE_LIMIT_BLOCK_REASON;
45
+ }
46
+
47
+ // Fallback window when the provider gave no reset time: retry after this long
48
+ // rather than waiting the full (unknown) limit window. Re-entering the hold on a
49
+ // still-limited retry is cheap, and a subscription limit almost always carries a
50
+ // reset time. Read at call-time so operators/tests can tune it.
51
+ const rateLimitFallbackMs = (): number =>
52
+ Number(process.env.AGENT_RELAY_RATE_LIMIT_FALLBACK_MS) || 60 * 60 * 1000;
53
+
54
+ /**
55
+ * When a rate-limit hold should be lifted. The provider's `resetAt` is
56
+ * authoritative; otherwise fall back to `enteredAt + fallback window`. Null only
57
+ * when neither is known (shouldn't happen — the runner always stamps enteredAt).
58
+ */
59
+ export function rateLimitResumeAt(state: ProviderStateView | null): number | null {
60
+ if (!state) return null;
61
+ if (typeof state.resetAt === "number" && Number.isFinite(state.resetAt)) return state.resetAt;
62
+ if (typeof state.enteredAt === "number" && Number.isFinite(state.enteredAt)) {
63
+ return state.enteredAt + rateLimitFallbackMs();
64
+ }
65
+ return null;
66
+ }
@@ -321,8 +321,8 @@ export const postWorkspaceAction: Handler = async (req, params) => {
321
321
  detail: cleanString(parsed.body.detail, "detail", { max: 4000 }),
322
322
  metadata: cleanMeta(parsed.body.metadata) ?? {},
323
323
  // No inline fallback: an absent strategy stays undefined and the shared core
324
- // (requestWorkspaceMerge) applies DEFAULT_MERGE_STRATEGY, identically to the
325
- // MCP land tool — one resolution point, no per-surface drift (#304).
324
+ // (requestWorkspaceMerge) applies the instance-aware default, identically to
325
+ // the MCP land tool — one resolution point, no per-surface drift.
326
326
  strategy: optionalEnum(parsed.body.strategy, "strategy", LAND_STRATEGIES) as WorkspaceMergeStrategy | undefined,
327
327
  deleteBranch: typeof parsed.body.deleteBranch === "boolean" ? parsed.body.deleteBranch : undefined,
328
328
  force: parsed.body.force === true,
@@ -0,0 +1,96 @@
1
+ import { createActivityEvent, listAgents } from "../db";
2
+ import { notifySystemMessage } from "../notify";
3
+ import { agentProviderState, isRateLimitHold, rateLimitResumeAt } from "../provider-state";
4
+
5
+ // #286 Phase 2: auto-resume agents held on a usage/rate-limit stall once their
6
+ // window resets. The runner reports the hold as `providerState: blocked` (reason
7
+ // rate_limit) with a resetAt; this sweep finds holds whose reset has passed and
8
+ // delivers a waking `continue` message. The agent's next turn clears the hold
9
+ // (the runner drops rateLimitHold on busy), so the blocked badge lifts on its own.
10
+ //
11
+ // Fan-out safe by construction: it resumes EVERY held agent past its reset, not
12
+ // one. Durable across restarts: resetAt lives in the agent's persisted meta, and
13
+ // the runner keeps re-asserting the hold while connected, so a relay restart just
14
+ // re-reads it. The cooldown tracker only debounces the brief window between
15
+ // sending the resume and the agent actually going busy.
16
+
17
+ const envMsOrDefault = (name: string, fallback: number): number => {
18
+ const v = Number(process.env[name]);
19
+ return Number.isFinite(v) && v >= 0 ? v : fallback;
20
+ };
21
+ // Don't re-send a resume to the same agent within this window — covers the gap
22
+ // between delivery and the agent flipping to busy (which clears the hold).
23
+ const resumeCooldownMs = () => envMsOrDefault("AGENT_RELAY_RATE_LIMIT_RESUME_COOLDOWN_MS", 5 * 60 * 1000);
24
+
25
+ const resumeTracker = new Map<string, number>();
26
+
27
+ export function resetRateLimitResumeTrackerForTests(): void {
28
+ resumeTracker.clear();
29
+ }
30
+
31
+ const RESUME_BODY =
32
+ "▶️ Your usage/rate limit has reset — resuming. Continue the task you were working on when the limit interrupted you. If you had already finished, no action is needed.";
33
+
34
+ export function resumeRateLimitedAgents(): Record<string, unknown> {
35
+ const now = Date.now();
36
+ const cooldown = resumeCooldownMs();
37
+ const seen = new Set<string>();
38
+ const resumed: string[] = [];
39
+ const waiting: string[] = [];
40
+ const cooling: string[] = [];
41
+
42
+ for (const agent of listAgents()) {
43
+ if (!isRateLimitHold(agent)) continue;
44
+ seen.add(agent.id);
45
+
46
+ const state = agentProviderState(agent);
47
+ const resumeAt = rateLimitResumeAt(state);
48
+ if (resumeAt !== null && now < resumeAt) { waiting.push(agent.id); continue; }
49
+
50
+ const lastResume = resumeTracker.get(agent.id);
51
+ if (lastResume !== undefined && now - lastResume < cooldown) { cooling.push(agent.id); continue; }
52
+ resumeTracker.set(agent.id, now);
53
+
54
+ try {
55
+ notifySystemMessage(agent.id, {
56
+ subject: "Usage limit reset — resume",
57
+ body: RESUME_BODY,
58
+ payload: {
59
+ kind: "agent.rate_limit_resume",
60
+ agentId: agent.id,
61
+ resetAt: state?.resetAt,
62
+ errorType: state?.errorType,
63
+ },
64
+ replyExpected: false,
65
+ });
66
+ } catch {
67
+ // A stale/unreachable target must never break the sweep; it store-aheads
68
+ // and retries next pass once the cooldown lapses.
69
+ resumeTracker.delete(agent.id);
70
+ continue;
71
+ }
72
+
73
+ resumed.push(agent.id);
74
+ createActivityEvent({
75
+ clientId: `rate-limit-resume-${agent.id}-${now}`,
76
+ kind: "state",
77
+ title: "Rate-limit hold resumed",
78
+ body: `Usage window reset — sent resume to ${agent.label ?? agent.id}`,
79
+ meta: agent.id,
80
+ icon: "ti-player-play",
81
+ view: "agents",
82
+ agentId: agent.id,
83
+ metadata: {
84
+ source: "server",
85
+ maintenanceJobId: "rate-limit-resume",
86
+ resetAt: state?.resetAt,
87
+ errorType: state?.errorType,
88
+ },
89
+ });
90
+ }
91
+
92
+ // Forget agents no longer held so a future hold resumes immediately.
93
+ for (const id of resumeTracker.keys()) if (!seen.has(id)) resumeTracker.delete(id);
94
+
95
+ return { held: seen.size, resumed, waiting, cooling, tracked: resumeTracker.size };
96
+ }
@@ -9,7 +9,7 @@
9
9
  // managed came-up-running reconcile (delegated to managed-running.ts), the single
10
10
  // status broadcast, the single timeline note, the spawned-child parent-wake, and
11
11
  // the first-registration audit row.
12
- import { createActivityEvent, getAgent, upsertAgent } from "../db";
12
+ import { createActivityEvent, getAgent, seedSpawnReplyObligation, upsertAgent } from "../db";
13
13
  import { emitAgentStatus } from "../sse";
14
14
  import { noteAgentTimelineEvent } from "../compaction-watch";
15
15
  import { notifyAgentReady } from "../agent-lifecycle-events";
@@ -59,6 +59,9 @@ export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): Agen
59
59
  if (agent.ready && !existing?.ready) notifyAgentReady(agent.id);
60
60
 
61
61
  if (!existing) {
62
+ // #413 — a spawned worker with a report-back contract gets a pending reply obligation to its
63
+ // spawner so it can't idle with its verdict undelivered (vent #121). First-registration only.
64
+ seedSpawnReplyObligation(agent.id, agent.spawnedBy, metaStr(agent.meta, "spawnRequestId"));
62
65
  createActivityEvent({
63
66
  clientId: `server-agent-${agent.id}-registered`,
64
67
  kind: "state",
@@ -22,7 +22,7 @@
22
22
  // OUT OF SCOPE (transport-edge, excluded from the parity facet): automatic memory injection
23
23
  // (needs request-derived context) and per-transport audit (HTTP domain row vs MCP tool-call row).
24
24
  import { isMechanicalMessageKind, isRecord, isReservedAgentId } from "agent-relay-sdk";
25
- import { ValidationError, getMessage, listAgents, sendMessageWithResult } from "../db";
25
+ import { ValidationError, getMessage, listAgents, sendMessageWithResult, channelObligationAnsweredBy } from "../db";
26
26
  import { planSend, type DeliveryReceipt } from "../agent-ref";
27
27
  import { emitMessageQueued, emitNewMessage } from "../sse";
28
28
  import { getLifecycleManager } from "../lifecycle-manager";
@@ -159,6 +159,18 @@ export function sendMessageService(
159
159
  // Resolve BEFORE authorize so both run against the canonical id (kills the HTTP-authorizes-raw
160
160
  // vs MCP-authorizes-resolved drift).
161
161
  const receipt = resolveSendTarget(input, input.from);
162
+ // Channel-answer linking: an agent answering the human through a channel bridge posts a body-less
163
+ // message to the channel agent (e.g. `agent-relay /message telegram '{"action":"reply",...}'`) that
164
+ // carries no reply/conversation linkage, so its reply obligation never clears and the Stop hook nags
165
+ // it into a redundant `/reply` that leaks back to the human as a second message. When the agent has
166
+ // exactly ONE open obligation on that channel this send is unambiguously the answer — link it here,
167
+ // at send time, so the obligation clears with no extra message. (`channelObligationAnsweredBy` is a
168
+ // no-op unless `to` resolved to a channel agent with a single open obligation; deciding at send time
169
+ // — not query time — keeps a later sibling conversation from retroactively mis-attributing it.)
170
+ if (!input.replyTo && !isMechanicalMessageKind(input.kind)) {
171
+ const answered = channelObligationAnsweredBy(input.from, input.to);
172
+ if (answered !== null) input.replyTo = answered;
173
+ }
162
174
  authorizeSend(input, ctx, opts, reservedSink);
163
175
 
164
176
  const result = sendMessageWithResult(input);
@@ -62,6 +62,15 @@ export interface SpawnAgentInput {
62
62
  profile?: string;
63
63
  prompt?: string;
64
64
  systemPromptAppend?: string;
65
+ /**
66
+ * #413 — does this spawn carry a report-back contract? When true, relay seeds a pending
67
+ * reply obligation from the spawner to the child at registration, so the child can't go
68
+ * idle until it delivers its result (vent #121). Defaults true for an agent-initiated spawn
69
+ * that hands the child a prompt (a coordinator handing a worker a task almost always wants
70
+ * the result back); off for system/dashboard/automation spawns (no caller to report to).
71
+ * Pass false to opt a fire-and-forget agent spawn out.
72
+ */
73
+ expectReply?: boolean;
65
74
  tags?: string[];
66
75
  capabilities?: string[];
67
76
  providerArgs?: string[];
@@ -204,6 +213,11 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
204
213
  ...(callerTeam ? { teamId: callerTeam.id } : {}),
205
214
  });
206
215
 
216
+ // #413 — default the report-back obligation ON for an agent coordinator handing a worker a
217
+ // task (callerId + a prompt); OFF for system/dashboard/automation spawns (no caller to report
218
+ // back to). Explicit input wins, so a coordinator can opt a fire-and-forget worker out.
219
+ const expectReply = input.expectReply ?? Boolean(callerId && input.prompt && input.prompt.trim());
220
+
207
221
  const command = createCommand({
208
222
  type: "agent.spawn",
209
223
  source: "system",
@@ -225,6 +239,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
225
239
  providerArgs: input.providerArgs ?? [],
226
240
  prompt: input.prompt,
227
241
  systemPromptAppend: input.systemPromptAppend,
242
+ ...(expectReply ? { expectReply } : {}),
228
243
  policyName: input.policyName,
229
244
  spawnRequestId,
230
245
  env,
@@ -2,6 +2,7 @@ import { createCommand } from "../commands-db";
2
2
  import { emitRelayEvent } from "../events";
3
3
  import { createActivityEvent, getAgent, listAgents, listWorkspaces } from "../db";
4
4
  import { notifySystemMessage } from "../notify";
5
+ import { isProviderBlocked } from "../provider-state";
5
6
  import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
6
7
 
7
8
  const envMsOrDefault = (name: string, fallback: number): number => {
@@ -64,7 +65,10 @@ export function reapTransientAgents(): Record<string, unknown> {
64
65
  for (const agent of listAgents()) {
65
66
  if (agent.lifecycle !== "transient") continue;
66
67
  seen.add(agent.id);
67
- if (agent.status !== "idle" || !agent.ready) { transientTracker.delete(agent.id); continue; }
68
+ // A held agent (rate-limit hold #286, or any provider-blocked state) reads as
69
+ // idle+ready but is NOT done — reaping it would kill work that's just waiting
70
+ // for the limit to reset. Treat it as not-idle; the clock restarts on resume.
71
+ if (agent.status !== "idle" || !agent.ready || isProviderBlocked(agent)) { transientTracker.delete(agent.id); continue; }
68
72
  const entry = transientTracker.get(agent.id) ?? { firstIdleAt: now };
69
73
  transientTracker.set(agent.id, entry);
70
74
  const idleForMs = now - entry.firstIdleAt;
@@ -91,6 +91,13 @@ interface BuildSpawnCommandOptions {
91
91
  providerArgs?: string[];
92
92
  prompt?: string;
93
93
  systemPromptAppend?: string;
94
+ /**
95
+ * #413 — when true, the child registering with this spawnRequestId gets a pending reply
96
+ * obligation back to its spawner, so the Stop hook refuses to let it idle until it delivers
97
+ * its result (closes the "spawned worker strands its verdict" gap, vent #121). Read at
98
+ * registration from the persisted spawn command params.
99
+ */
100
+ expectReply?: boolean;
94
101
  profile?: string;
95
102
  agentProfile?: unknown;
96
103
  headless?: boolean;
@@ -138,6 +145,7 @@ export function buildSpawnCommand(opts: BuildSpawnCommandOptions): Record<string
138
145
  ...def(opts.providerArgs, "providerArgs"),
139
146
  ...def(opts.prompt, "prompt"),
140
147
  ...def(opts.systemPromptAppend, "systemPromptAppend"),
148
+ ...def(opts.expectReply, "expectReply"),
141
149
  ...def(opts.headless, "headless"),
142
150
  ...def(opts.policyName, "policyName"),
143
151
  ...def(opts.automationId, "automationId"),
@@ -167,8 +167,8 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
167
167
  const result = requestWorkspaceMerge(workspace, {
168
168
  requestedBy: agentId ?? "dashboard",
169
169
  // Pass the strategy through verbatim (undefined when unset) — the single
170
- // default lives in requestWorkspaceMerge, so both land surfaces resolve it
171
- // identically instead of each applying its own fallback (#304).
170
+ // config-aware default lives in requestWorkspaceMerge, so land surfaces
171
+ // resolve it identically instead of each applying its own fallback.
172
172
  strategy: input.strategy,
173
173
  deleteBranch: input.deleteBranch !== false,
174
174
  prTitle: input.prTitle,
@@ -176,7 +176,10 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
176
176
  releaseMergeLease({ repoRoot: workspace.repoRoot, workspaceId: workspace.id });
177
177
  return { ok: false, status: 404, error: "workspace not found" };
178
178
  }
179
- const effectiveStrategy = opts.strategy ?? DEFAULT_MERGE_STRATEGY;
179
+ const effectiveStrategy = resolveLandStrategy({
180
+ explicit: opts.strategy,
181
+ instanceStrategy: getLandingConfig().strategy,
182
+ });
180
183
 
181
184
  // Slice 6 — issue auto-linkage: append "Closes #N" to the PR body when the
182
185
  // workspace metadata carries an issue number, WITHOUT clobbering an explicit body.
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{B as t,Dt as n,Ft as r,G as i,I as a,J as o,P as s,Tn as c,Un as l,bn as u,g as d,i as f,jn as p,kn as m,l as h,m as g,ot as _}from"./lucide-react-ChOGD4vc.js";import{o as v,t as y}from"./store-BlsqvkSE.js";import{B as b,G as x,H as ee,W as te,a as S,d as C,i as ne,n as re}from"./display-CXhnBwGf.js";import{o as w,r as T}from"./dist-CSISzAHD.js";import{t as E}from"./dist-DvLO--_O.js";import{i as D,n as O}from"./dist-MdPDPcyo.js";import{t as k}from"./dist-BiVJ8IWp.js";import{t as A}from"./dist-BIAvrITs.js";import{a as j,i as M,n as N,o as P,r as ie,s as ae,t as F}from"./es2015-Cu7l1536.js";import{a as I,i as oe,n as se,r as L,t as R}from"./dist-svixSeDF.js";import{t as z}from"./badge-BR4bvEsy.js";import{t as B}from"./button-BwqJZS4O.js";import{t as ce}from"./copy-button-nbDhTE0M.js";import{B as le,H as V,P as ue,S as H,U,V as W,d as de,g as fe,h as pe,l as me,m as he,p as ge,u as _e,x as G}from"./index-BRi2QbMp.js";import{n as ve,t as ye}from"./card-C7JtSyuM.js";import{n as be,t as xe}from"./branch-state-badge-CzlsNakd.js";var K=e(l(),1),q=p(),J=`Popover`,[Se,Ce]=D(J,[I]),Y=I(),[we,X]=Se(J),Te=e=>{let{__scopePopover:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!1}=e,s=Y(t),c=K.useRef(null),[l,u]=K.useState(!1),[d,f]=k({prop:r,defaultProp:i??!1,onChange:a,caller:J});return(0,q.jsx)(oe,{...s,children:(0,q.jsx)(we,{scope:t,contentId:ae(),triggerRef:c,open:d,onOpenChange:f,onOpenToggle:K.useCallback(()=>f(e=>!e),[f]),hasCustomAnchor:l,onCustomAnchorAdd:K.useCallback(()=>u(!0),[]),onCustomAnchorRemove:K.useCallback(()=>u(!1),[]),modal:o,children:n})})};Te.displayName=J;var Ee=`PopoverAnchor`,De=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Ee,n),a=Y(n),{onCustomAnchorAdd:o,onCustomAnchorRemove:s}=i;return K.useEffect(()=>(o(),()=>s()),[o,s]),(0,q.jsx)(R,{...a,...r,ref:t})});De.displayName=Ee;var Oe=`PopoverTrigger`,ke=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Oe,n),a=Y(n),o=w(t,i.triggerRef),s=(0,q.jsx)(E.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":i.open,"aria-controls":i.contentId,"data-state":Ve(i.open),...r,ref:o,onClick:O(e.onClick,i.onOpenToggle)});return i.hasCustomAnchor?s:(0,q.jsx)(R,{asChild:!0,...a,children:s})});ke.displayName=Oe;var Z=`PopoverPortal`,[Ae,je]=Se(Z,{forceMount:void 0}),Me=e=>{let{__scopePopover:t,forceMount:n,children:r,container:i}=e,a=X(Z,t);return(0,q.jsx)(Ae,{scope:t,forceMount:n,children:(0,q.jsx)(A,{present:n||a.open,children:(0,q.jsx)(M,{asChild:!0,container:i,children:r})})})};Me.displayName=Z;var Q=`PopoverContent`,Ne=K.forwardRef((e,t)=>{let n=je(Q,e.__scopePopover),{forceMount:r=n.forceMount,...i}=e,a=X(Q,e.__scopePopover);return(0,q.jsx)(A,{present:r||a.open,children:a.modal?(0,q.jsx)(Fe,{...i,ref:t}):(0,q.jsx)(Ie,{...i,ref:t})})});Ne.displayName=Q;var Pe=T(`PopoverContent.RemoveScroll`),Fe=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(null),i=w(t,r),a=K.useRef(!1);return K.useEffect(()=>{let e=r.current;if(e)return F(e)},[]),(0,q.jsx)(N,{as:Pe,allowPinchZoom:!0,children:(0,q.jsx)(Le,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:O(e.onCloseAutoFocus,e=>{e.preventDefault(),a.current||n.triggerRef.current?.focus()}),onPointerDownOutside:O(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;a.current=t.button===2||n},{checkForDefaultPrevented:!1}),onFocusOutside:O(e.onFocusOutside,e=>e.preventDefault(),{checkForDefaultPrevented:!1})})})}),Ie=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(!1),i=K.useRef(!1);return(0,q.jsx)(Le,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),Le=K.forwardRef((e,t)=>{let{__scopePopover:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:a,disableOutsidePointerEvents:o,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onInteractOutside:u,...d}=e,f=X(Q,n),p=Y(n);return ie(),(0,q.jsx)(j,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:a,children:(0,q.jsx)(P,{asChild:!0,disableOutsidePointerEvents:o,onInteractOutside:u,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onDismiss:()=>f.onOpenChange(!1),children:(0,q.jsx)(L,{"data-state":Ve(f.open),role:`dialog`,id:f.contentId,...p,...d,ref:t,style:{...d.style,"--radix-popover-content-transform-origin":`var(--radix-popper-transform-origin)`,"--radix-popover-content-available-width":`var(--radix-popper-available-width)`,"--radix-popover-content-available-height":`var(--radix-popper-available-height)`,"--radix-popover-trigger-width":`var(--radix-popper-anchor-width)`,"--radix-popover-trigger-height":`var(--radix-popper-anchor-height)`}})})})}),$=`PopoverClose`,Re=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X($,n);return(0,q.jsx)(E.button,{type:`button`,...r,ref:t,onClick:O(e.onClick,()=>i.onOpenChange(!1))})});Re.displayName=$;var ze=`PopoverArrow`,Be=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=Y(n);return(0,q.jsx)(se,{...i,...r,ref:t})});Be.displayName=ze;function Ve(e){return e?`open`:`closed`}var He=Te,Ue=ke,We=Me,Ge=Ne;function Ke({...e}){return(0,q.jsx)(He,{"data-slot":`popover`,...e})}function qe({...e}){return(0,q.jsx)(Ue,{"data-slot":`popover-trigger`,...e})}function Je({className:e,align:t=`center`,sideOffset:n=4,...r}){return(0,q.jsx)(We,{children:(0,q.jsx)(Ge,{"data-slot":`popover-content`,align:t,sideOffset:n,className:m(`z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r})})}function Ye({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-header`,className:m(`flex flex-col gap-0.5 text-sm`,e),...t})}function Xe({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-title`,className:m(`font-medium`,e),...t})}function Ze({agent:e,childAgents:a=[],parentAgent:c}){let l=v();y(e=>e.set);let u=y(e=>e.switchView),f=y(e=>e.openInboxThread),p=y(e=>e.openAgentDetail),m=y(e=>e.openPairInvite),w=y(e=>e.openRename),T=y(e=>e.openConfirm),E=y(e=>e.doAgentAction),D=y(e=>e.doDeleteAgent),O=y(e=>e.openFilesForAgent),k=y(e=>e.workspaceAction),A=y(e=>e.openWorkspaceFocus),{terminalOpen:j,terminalTarget:M,terminalOpening:N,openTerminal:P,closeTerminal:ie}=be(e.id),ae=le(),F=ne(l,e,ue(e),ae[e.id]||null),I=S(e,`shutdown`),oe=S(e,`compact`),se=S(e,`clearContext`),L=!!(e.providerCapabilities?.terminal?.live?.read||e.providerCapabilities?.terminal?.attach?.create),R=!!(e.providerCapabilities?.terminal?.live?.write||e.providerCapabilities?.model?.provider===`claude`),V=re(e),U=c?C(c):e.spawnedBy,W=a.length>0;return(0,q.jsxs)(q.Fragment,{children:[(0,q.jsx)(ye,{className:`group hover:border-zinc-600 transition-colors cursor-pointer`,onClick:()=>p(e),children:(0,q.jsxs)(ve,{className:`p-4`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2.5 mb-2.5`,children:[(0,q.jsx)(H,{agent:e,now:l,className:`mt-0.5`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,q.jsx)(`div`,{className:`text-sm font-medium truncate`,children:C(e)}),(0,q.jsx)(`div`,{className:`text-xs mt-0.5 ${te(F.tone)}`,children:F.label}),typeof e.meta?.cwd==`string`&&e.meta.cwd&&(0,q.jsx)(`button`,{type:`button`,className:`mt-0.5 block max-w-full truncate font-mono text-[10px] text-muted-foreground hover:text-foreground`,title:`Open ${e.meta.cwd} in Files`,onClick:t=>{t.stopPropagation(),O(e)},children:b(e.meta.cwd)})]}),e.branchState&&e.branchWorkspaceId&&(0,q.jsx)(xe,{state:e.branchState,className:`mt-0.5 shrink-0`,onClick:t=>{t.stopPropagation(),A(e.branchWorkspaceId)}})]}),F.badges.length>0&&(0,q.jsx)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:F.badges.map(e=>(0,q.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded-full font-medium ${e.className}`,children:e.label},e.label))}),(W||e.spawnedBy&&U)&&(0,q.jsxs)(`div`,{className:`mb-2 flex flex-wrap gap-1.5`,onClick:e=>e.stopPropagation(),children:[W&&(0,q.jsxs)(Ke,{children:[(0,q.jsx)(qe,{asChild:!0,children:(0,q.jsxs)(`button`,{type:`button`,className:`inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border border-sky-500/30 bg-sky-500/10 px-2 text-[10px] font-medium text-sky-300 transition-colors hover:bg-sky-500/15`,title:`${a.length} live child ${a.length===1?`agent`:`agents`}`,children:[(0,q.jsx)(h,{className:`h-3 w-3`}),(0,q.jsx)(`span`,{children:a.length}),(0,q.jsx)(`span`,{className:`flex -space-x-1`,children:a.slice(0,4).map(e=>(0,q.jsx)(H,{agent:e,now:l,className:`h-2 w-2 ring-1 ring-background`},e.id))})]})}),(0,q.jsxs)(Je,{align:`start`,className:`w-80`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(Ye,{children:(0,q.jsx)(Xe,{children:`Team`})}),(0,q.jsx)(`div`,{className:`space-y-1.5`,children:a.map(e=>(0,q.jsxs)(`div`,{className:`rounded-md border border-border bg-card/60 p-2`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,q.jsx)(H,{agent:e,now:l,className:`mt-1`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,q.jsx)(`div`,{className:`truncate text-xs font-medium`,children:C(e)}),(0,q.jsx)(`div`,{className:`truncate text-[10px] text-muted-foreground`,children:e.providerCapabilities?.model?.alias||e.providerCapabilities?.model?.id||e.providerCapabilities?.model?.provider||e.status})]}),(0,q.jsx)(z,{variant:`outline`,className:`h-4 px-1.5 text-[9px]`,children:e.status})]}),(0,q.jsxs)(`div`,{className:`mt-2 flex gap-1`,children:[(0,q.jsx)(B,{size:`xs`,variant:`outline`,onClick:()=>p(e),children:`Open`}),(0,q.jsxs)(B,{size:`xs`,variant:`ghost`,onClick:()=>{f(e.id),u(`chat`)},children:[(0,q.jsx)(o,{className:`h-3 w-3`}),`Chat`]})]})]},e.id))})]})]}),e.spawnedBy&&U&&(0,q.jsx)(z,{variant:`outline`,className:`max-w-full text-[10px] text-muted-foreground`,children:(0,q.jsxs)(`span`,{className:`truncate`,children:[`spawned by `,U]})})]}),(e.tags.length>0||x(e.capabilities).length>0)&&(0,q.jsxs)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:[e.tags.slice(0,3).map(e=>(0,q.jsx)(z,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e)),x(e.capabilities).slice(0,2).map(e=>(0,q.jsx)(z,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))]}),(0,q.jsxs)(`div`,{className:`space-y-2 mb-2`,children:[(0,q.jsx)(pe,{agent:e}),(0,q.jsx)(he,{agent:e,compact:!0}),(0,q.jsx)(fe,{agent:e})]}),(0,q.jsx)(`div`,{className:`text-xs text-muted-foreground`,children:e.id!==`user`&&e.id!==`system`&&ee(l,e.lastSeen)}),(0,q.jsxs)(`div`,{className:`flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(ce,{value:e.id,label:`Copy agent ID`,size:`icon`,className:`h-7 w-7`,iconClassName:`w-3 h-3`}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Chat`,onClick:()=>{f(e.id),u(`chat`)},children:(0,q.jsx)(o,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Pair`,onClick:()=>m(e.id),children:(0,q.jsx)(_,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Rename`,onClick:()=>w(e),children:(0,q.jsx)(t,{className:`w-3 h-3`})}),L&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Terminal`,disabled:N,onClick:()=>void P(),children:(0,q.jsx)(d,{className:`w-3 h-3`})}),(e.branchState===`changes`||e.branchState===`idle`)&&e.branchWorkspaceId&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-amber-400 hover:text-amber-300`,title:`Mark workspace ready — hand off to the auto-merge`,onClick:()=>T(`Mark Workspace Ready`,`Mark ${C(e)}'s branch ready to land? The relay auto-merge will rebase and land it.`,()=>k(e.branchWorkspaceId,`ready`)),children:(0,q.jsx)(n,{className:`w-3 h-3`})}),oe&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Compact`,onClick:()=>T(`Compact Agent`,`Compact context for ${C(e)}?`,()=>E(e,`compact`,`agents-grid`)),children:(0,q.jsx)(i,{className:`w-3 h-3`})}),se&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Clear Context`,onClick:()=>T(`Clear Context`,`Clear context for ${C(e)}?`,()=>E(e,`clearContext`,`agents-grid`)),children:(0,q.jsx)(r,{className:`w-3 h-3`})}),I&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Shutdown`,onClick:()=>T(`Shutdown Agent`,`Shutdown ${C(e)}?`,()=>E(e,`shutdown`,`agents-grid`)),children:(0,q.jsx)(s,{className:`w-3 h-3`})}),V&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Forget stale agent`,onClick:()=>T(`Forget Agent`,`Forget ${C(e)}?`,()=>D(e.id)),children:(0,q.jsx)(g,{className:`w-3 h-3`})})]})]})}),M&&(0,q.jsx)(ge,{open:j,onOpenChange:ie,orchestratorId:M.orchestratorId,session:M.session,interactive:M.interactive||R})]})}function Qe(){let e=y(e=>e.agentPresetFilter),t=y(e=>e.agentStatusFilter),n=y(e=>e.agentTagFilter),r=y(e=>e.agentHostFilter),i=y(e=>e.agentSort),o=y(e=>e.agentSortDir),s=y(e=>e.showOffline),l=y(e=>e.set),d=y(e=>e.openAgentSpawn),p=y(e=>e.agentsById),m=v(),h=W(),g=U(),_=V(),b=me(h,m),x=de(h,m),ee=e||t||n||r;function te(){l({agentPresetFilter:``,agentStatusFilter:``,agentTagFilter:``,agentHostFilter:``})}function S(){l({agentSortDir:o===`asc`?`desc`:`asc`})}return(0,q.jsxs)(`div`,{className:`space-y-4`,children:[(0,q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,q.jsxs)(`select`,{value:e,onChange:e=>l({agentPresetFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`All agents`}),(0,q.jsx)(`option`,{value:`active`,children:`Active`}),(0,q.jsx)(`option`,{value:`claude`,children:`Claude`}),(0,q.jsx)(`option`,{value:`codex`,children:`Codex`}),(0,q.jsx)(`option`,{value:`paired`,children:`Paired`}),(0,q.jsx)(`option`,{value:`unpaired`,children:`Unpaired`}),(0,q.jsx)(`option`,{value:`offline_stale`,children:`Offline / Stale`})]}),(0,q.jsxs)(`select`,{value:t,onChange:e=>l({agentStatusFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any status`}),(0,q.jsx)(`option`,{value:`online`,children:`Online`}),(0,q.jsx)(`option`,{value:`idle`,children:`Idle`}),(0,q.jsx)(`option`,{value:`busy`,children:`Busy`}),(0,q.jsx)(`option`,{value:`offline`,children:`Offline`}),(0,q.jsx)(`option`,{value:`starting`,children:`Starting`})]}),g.length>0&&(0,q.jsxs)(`select`,{value:n,onChange:e=>l({agentTagFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any tag`}),g.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),_.length>1&&(0,q.jsxs)(`select`,{value:r,onChange:e=>l({agentHostFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any host`}),_.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),(0,q.jsxs)(`select`,{value:i,onChange:e=>l({agentSort:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:`status`,children:`Sort: Status`}),(0,q.jsx)(`option`,{value:`name`,children:`Sort: Name`}),(0,q.jsx)(`option`,{value:`lastSeen`,children:`Sort: Last seen`}),(0,q.jsx)(`option`,{value:`created`,children:`Sort: Created`})]}),(0,q.jsx)(B,{size:`icon`,variant:`outline`,className:`h-8 w-8`,title:`Toggle sort direction`,onClick:S,children:o===`asc`?(0,q.jsx)(u,{className:`w-3.5 h-3.5`}):(0,q.jsx)(c,{className:`w-3.5 h-3.5`})}),(0,q.jsxs)(`label`,{className:`flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer select-none`,children:[(0,q.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>l({showOffline:e.target.checked}),className:`rounded`}),`Show offline`]}),ee&&(0,q.jsxs)(B,{size:`sm`,variant:`ghost`,className:`h-8 text-muted-foreground hover:text-foreground gap-1`,onClick:te,children:[(0,q.jsx)(f,{className:`w-3 h-3`}),`Clear filters`]}),(0,q.jsx)(`div`,{className:`ml-auto`,children:(0,q.jsxs)(B,{size:`sm`,onClick:d,className:`gap-1`,children:[(0,q.jsx)(a,{className:`w-3.5 h-3.5`}),`Spawn`]})})]}),x.length===0?(0,q.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No agents match the current filters.`}):(0,q.jsx)(`div`,{className:`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3`,children:x.map(e=>(0,q.jsx)(Ze,{agent:e,parentAgent:e.spawnedBy?p[e.spawnedBy]??null:null,childAgents:_e(b.get(e.id)??[])},e.id))})]})}export{Qe as AgentsView};
2
- //# sourceMappingURL=agents-DV5y_tF1.js.map
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{$t as t,Jt as n,Mn as r,Un as i,a,bt as o,jn as s,kn as c,p as l}from"./lucide-react-ChOGD4vc.js";import{t as u}from"./store-BlsqvkSE.js";var d=e(i(),1);function f(e){let t=u(e=>e.showError),[n,i]=(0,d.useState)(!1),[a,o]=(0,d.useState)(null),[s,c]=(0,d.useState)(!1);async function l(){if(!(!e||s)){c(!0);try{o(await r(`POST`,`/agents/`+encodeURIComponent(e)+`/terminal-session`)),i(!0)}catch(e){t(`Terminal Failed`,e.message)}finally{c(!1)}}}function f(t){if(t){i(!0);return}i(!1);let n=a;o(null),e&&n?.mode===`guest`&&r(`DELETE`,`/agents/`+encodeURIComponent(e)+`/terminal-session/`+encodeURIComponent(n.session)).catch(()=>{})}return{terminalOpen:n,terminalTarget:a,terminalOpening:s,openTerminal:l,closeTerminal:f}}var p=s(),m={idle:{label:`idle`,tip:`Branch worktree — nothing to land yet.`,className:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,Icon:n},changes:{label:`changes`,tip:`Unlanded work in this branch — use "Mark workspace ready" to land it.`,className:`bg-amber-500/10 text-amber-400 border-amber-500/20`,Icon:o},ready:{label:`ready`,tip:`Handed off — the relay auto-merge will land it (~2 min sweep).`,className:`bg-blue-500/10 text-blue-400 border-blue-500/20`,Icon:t},steward:{label:`steward`,tip:`Under reconciliation — a steward is landing it. No action needed.`,className:`bg-orange-500/10 text-orange-400 border-orange-500/20`,Icon:a},blocked:{label:`blocked`,tip:`Escalated — the auto-merge/steward path is stuck. Needs human attention.`,className:`bg-red-500/10 text-red-400 border-red-500/20`,Icon:l}};function h({state:e,onClick:t,iconOnly:n=!1,className:r}){let i=m[e],{Icon:a}=i,o=!!t,s=o?`${i.tip} Click to open this workspace.`:i.tip;return(0,p.jsxs)(`span`,{role:o?`button`:void 0,tabIndex:o?0:void 0,title:s,onClick:t,onKeyDown:o?e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),t?.(e))}:void 0,className:c(`inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium leading-none`,i.className,o&&`cursor-pointer hover:brightness-125 focus:outline-none focus:ring-1 focus:ring-current`,r),children:[(0,p.jsx)(a,{className:`h-2.5 w-2.5 shrink-0`}),!n&&(0,p.jsx)(`span`,{children:i.label})]})}export{f as n,h as t};
2
- //# sourceMappingURL=branch-state-badge-CzlsNakd.js.map
@@ -1,2 +0,0 @@
1
- import"./chunk-CilyBKbf.js";import{Un as e,jn as t,kn as n}from"./lucide-react-ChOGD4vc.js";e();var r=t();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-content`,className:n(`px-4 group-data-[size=sm]/card:px-3`,e),...t})}export{o as i,s as n,a as r,i as t};
2
- //# sourceMappingURL=card-C7JtSyuM.js.map