agent-relay-server 0.11.9 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.11.9",
3
+ "version": "0.12.1",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "CONTRIBUTING.md"
33
33
  ],
34
34
  "dependencies": {
35
- "agent-relay-sdk": "0.2.5"
35
+ "agent-relay-sdk": "0.2.6"
36
36
  },
37
37
  "scripts": {
38
38
  "prepack": "bun run build:dashboard >&2",
package/public/index.html CHANGED
@@ -10591,6 +10591,7 @@ function isAgentStale(now, agent) {
10591
10591
  function agentSupportsControlAction(agent, action) {
10592
10592
  if (!agent || isBuiltInAgent(agent) || isChannelAgent(agent)) return false;
10593
10593
  if (action === "resume") return false;
10594
+ if (action === "interrupt") return agent.providerCapabilities?.liveSession?.interrupt === true && agent.status === "busy";
10594
10595
  if (agentType(agent) === "claude" && (action === "restart" || action === "shutdown" || action === "compact" || action === "clearContext") && !agentHasTmuxSession(agent)) return false;
10595
10596
  const lifecycle = agent.providerCapabilities?.lifecycle;
10596
10597
  if (lifecycle) {
@@ -124029,18 +124030,18 @@ function useFileListing(orchestratorId, selectedPath, git = false) {
124029
124030
  }
124030
124031
  function RawTextContent({ content, line, lineRef }) {
124031
124032
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
124032
- className: "min-w-full py-2 font-mono text-xs leading-5",
124033
+ className: "w-full py-2 font-mono text-xs leading-5",
124033
124034
  children: content.split("\n").map((text, index) => {
124034
124035
  const lineNumber = index + 1;
124035
124036
  const active = lineNumber === line;
124036
124037
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
124037
124038
  ref: active ? lineRef : void 0,
124038
- className: `grid grid-cols-[4rem_minmax(0,1fr)] px-2 ${active ? "bg-primary/15" : ""}`,
124039
+ className: `grid grid-cols-[2.5rem_minmax(0,1fr)] px-1 sm:grid-cols-[4rem_minmax(0,1fr)] sm:px-2 ${active ? "bg-primary/15" : ""}`,
124039
124040
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
124040
- className: "select-none pr-3 text-right text-muted-foreground",
124041
+ className: "select-none pr-2 text-right text-muted-foreground sm:pr-3",
124041
124042
  children: lineNumber
124042
124043
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
124043
- className: "overflow-visible whitespace-pre-wrap break-words",
124044
+ className: "overflow-hidden whitespace-pre-wrap break-words",
124044
124045
  children: text || " "
124045
124046
  })]
124046
124047
  }, lineNumber);
@@ -124374,7 +124375,7 @@ function FileBrowser({ overlay = false }) {
124374
124375
  className: overlay ? "flex h-full flex-col" : "flex h-[calc(100dvh-5rem)] flex-col gap-3 md:gap-4",
124375
124376
  children: [
124376
124377
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
124377
- className: "flex flex-wrap items-center gap-2",
124378
+ className: `flex flex-wrap items-center gap-2 ${selectedPath ? "hidden md:flex" : ""}`,
124378
124379
  children: [
124379
124380
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
124380
124381
  className: "flex items-center gap-2 text-lg font-semibold",
@@ -124403,7 +124404,7 @@ function FileBrowser({ overlay = false }) {
124403
124404
  ]
124404
124405
  }),
124405
124406
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
124406
- className: "flex flex-wrap items-center gap-2",
124407
+ className: `flex flex-wrap items-center gap-2 ${selectedPath ? "hidden md:flex" : ""}`,
124407
124408
  children: [
124408
124409
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
124409
124410
  value: pathDraft,
@@ -124461,7 +124462,7 @@ function FileBrowser({ overlay = false }) {
124461
124462
  ]
124462
124463
  }),
124463
124464
  segments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
124464
- className: "flex flex-wrap items-center gap-1 text-xs text-muted-foreground",
124465
+ className: `flex flex-wrap items-center gap-1 text-xs text-muted-foreground ${selectedPath ? "hidden md:flex" : ""}`,
124465
124466
  children: segments.map((segment, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
124466
124467
  type: "button",
124467
124468
  className: "rounded px-1.5 py-0.5 font-mono hover:bg-accent hover:text-foreground",
@@ -124478,7 +124479,7 @@ function FileBrowser({ overlay = false }) {
124478
124479
  children: visibleError
124479
124480
  }),
124480
124481
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
124481
- className: "grid min-h-0 flex-1 overflow-hidden rounded-md border border-border md:grid-cols-[22rem_minmax(0,1fr)]",
124482
+ className: `grid min-h-0 flex-1 overflow-hidden md:rounded-md md:border md:border-border md:grid-cols-[22rem_minmax(0,1fr)] ${selectedPath ? "" : "rounded-md border border-border"}`,
124482
124483
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
124483
124484
  className: `min-h-0 overflow-auto border-b border-border bg-card md:border-b-0 md:border-r ${selectedPath ? "hidden md:block" : ""}`,
124484
124485
  children: [
@@ -124567,6 +124568,101 @@ function Textarea({ className, ...props }) {
124567
124568
  });
124568
124569
  }
124569
124570
  //#endregion
124571
+ //#region src/lib/slash-commands.ts
124572
+ var RELAY_SLASH_COMMANDS = [
124573
+ {
124574
+ name: "/reply",
124575
+ source: "skill",
124576
+ description: "Reply to a relay message by id",
124577
+ argumentHint: "<messageId> <body>"
124578
+ },
124579
+ {
124580
+ name: "/message",
124581
+ source: "skill",
124582
+ description: "Send a relay message to an agent, label, tag, or broadcast",
124583
+ argumentHint: "<target> <body>"
124584
+ },
124585
+ {
124586
+ name: "/pair",
124587
+ source: "skill",
124588
+ description: "Start or manage a two-agent pair session"
124589
+ },
124590
+ {
124591
+ name: "/react",
124592
+ source: "skill",
124593
+ description: "Add a reaction to a relay message",
124594
+ argumentHint: "<messageId> <emoji>"
124595
+ },
124596
+ {
124597
+ name: "/read-message",
124598
+ source: "skill",
124599
+ description: "Fetch a full relay message by id",
124600
+ argumentHint: "<messageId>"
124601
+ },
124602
+ {
124603
+ name: "/send-claimable",
124604
+ source: "skill",
124605
+ description: "Enqueue a claimable work item for one agent to claim"
124606
+ },
124607
+ {
124608
+ name: "/status",
124609
+ source: "skill",
124610
+ description: "Show this agent's relay status"
124611
+ },
124612
+ {
124613
+ name: "/tags",
124614
+ source: "skill",
124615
+ description: "List or update this agent's relay tags"
124616
+ },
124617
+ {
124618
+ name: "/label",
124619
+ source: "skill",
124620
+ description: "Read, set, or clear this agent's relay label"
124621
+ },
124622
+ {
124623
+ name: "/disconnect",
124624
+ source: "skill",
124625
+ description: "End the current relay pair session"
124626
+ }
124627
+ ];
124628
+ var CLAUDE_BUILTIN_SLASH_COMMANDS = [
124629
+ {
124630
+ name: "/clear",
124631
+ source: "builtin",
124632
+ description: "Clear conversation history and free up context"
124633
+ },
124634
+ {
124635
+ name: "/compact",
124636
+ source: "builtin",
124637
+ description: "Compact the conversation to reclaim context",
124638
+ argumentHint: "[instructions]"
124639
+ },
124640
+ {
124641
+ name: "/model",
124642
+ source: "builtin",
124643
+ description: "Switch the active model",
124644
+ argumentHint: "[model]"
124645
+ },
124646
+ {
124647
+ name: "/help",
124648
+ source: "builtin",
124649
+ description: "Show help"
124650
+ }
124651
+ ];
124652
+ var CODEX_BUILTIN_SLASH_COMMANDS = [{
124653
+ name: "/compact",
124654
+ source: "builtin",
124655
+ description: "Compact the conversation to reclaim context"
124656
+ }, {
124657
+ name: "/new",
124658
+ source: "builtin",
124659
+ description: "Start a new thread"
124660
+ }];
124661
+ /** Slash-command catalog for a provider's chat-input autocomplete palette. */
124662
+ function providerSlashCommands(provider) {
124663
+ return [...provider === "claude" ? CLAUDE_BUILTIN_SLASH_COMMANDS : provider === "codex" ? CODEX_BUILTIN_SLASH_COMMANDS : [], ...RELAY_SLASH_COMMANDS];
124664
+ }
124665
+ //#endregion
124570
124666
  //#region src/components/views/chat.tsx
124571
124667
  function formatBytes(size) {
124572
124668
  if (size < 1024) return `${size} B`;
@@ -125163,7 +125259,7 @@ function StatusMarker({ event }) {
125163
125259
  ]
125164
125260
  });
125165
125261
  }
125166
- function BusyIndicator({ blockedLabel }) {
125262
+ function BusyIndicator({ blockedLabel, onInterrupt }) {
125167
125263
  if (blockedLabel) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125168
125264
  className: "flex justify-start mb-3",
125169
125265
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -125178,16 +125274,78 @@ function BusyIndicator({ blockedLabel }) {
125178
125274
  className: "flex justify-start mb-3",
125179
125275
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125180
125276
  className: "flex items-center gap-2 rounded-2xl rounded-bl-sm bg-card ring-1 ring-foreground/10 px-4 py-2.5",
125181
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125182
- className: "flex items-center gap-1",
125277
+ children: [
125278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125279
+ className: "flex items-center gap-1",
125280
+ children: [
125281
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-yellow-400 animate-[pulse_1.4s_ease-in-out_infinite]" }),
125282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-yellow-400 animate-[pulse_1.4s_ease-in-out_0.2s_infinite]" }),
125283
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-yellow-400 animate-[pulse_1.4s_ease-in-out_0.4s_infinite]" })
125284
+ ]
125285
+ }),
125286
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125287
+ className: "text-xs text-muted-foreground",
125288
+ children: "working"
125289
+ }),
125290
+ onInterrupt && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125291
+ onClick: onInterrupt,
125292
+ title: "Interrupt",
125293
+ className: "ml-1 flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs text-muted-foreground hover:text-red-300 hover:bg-red-500/10 transition-colors",
125294
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleStop, { className: "w-3.5 h-3.5" }), "Stop"]
125295
+ })
125296
+ ]
125297
+ })
125298
+ });
125299
+ }
125300
+ function ActivityTrace({ steps }) {
125301
+ const [expanded, setExpanded] = (0, import_react.useState)(false);
125302
+ const last = steps[steps.length - 1];
125303
+ if (!last) return null;
125304
+ const summary = (last.label || last.text || "").replace(/\s+/g, " ").trim();
125305
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125306
+ className: "flex justify-start mb-2",
125307
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125308
+ className: "max-w-[85%] md:max-w-[75%] min-w-0",
125309
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125310
+ onClick: () => setExpanded((v) => !v),
125311
+ className: "flex items-center gap-1.5 text-xs text-muted-foreground/70 hover:text-muted-foreground transition-colors w-full text-left",
125183
125312
  children: [
125184
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-yellow-400 animate-[pulse_1.4s_ease-in-out_infinite]" }),
125185
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-yellow-400 animate-[pulse_1.4s_ease-in-out_0.2s_infinite]" }),
125186
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-yellow-400 animate-[pulse_1.4s_ease-in-out_0.4s_infinite]" })
125313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Brain, { className: "w-3.5 h-3.5 shrink-0" }),
125314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125315
+ className: "truncate",
125316
+ children: expanded ? "Activity" : summary.length > 80 ? `${summary.slice(0, 79)}…` : summary || "thinking"
125317
+ }),
125318
+ steps.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
125319
+ className: "opacity-60 shrink-0",
125320
+ children: [
125321
+ "· ",
125322
+ steps.length,
125323
+ " steps"
125324
+ ]
125325
+ }),
125326
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { className: cn$2("w-3 h-3 shrink-0 transition-transform", expanded && "rotate-90") })
125187
125327
  ]
125188
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125189
- className: "text-xs text-muted-foreground",
125190
- children: "working"
125328
+ }), expanded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125329
+ className: "mt-1.5 space-y-1.5 border-l border-foreground/10 pl-2.5",
125330
+ children: steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125331
+ className: "text-xs leading-relaxed",
125332
+ children: step.kind === "reasoning" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125333
+ className: "italic text-muted-foreground/80 whitespace-pre-wrap break-words",
125334
+ children: step.text
125335
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
125336
+ className: "text-muted-foreground flex items-start gap-1.5",
125337
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Terminal, { className: "w-3 h-3 mt-0.5 shrink-0 opacity-70" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
125338
+ className: "min-w-0 break-words",
125339
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125340
+ className: "font-medium",
125341
+ children: step.label || "tool"
125342
+ }), step.text ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
125343
+ className: "opacity-70",
125344
+ children: [" — ", step.text]
125345
+ }) : null]
125346
+ })]
125347
+ })
125348
+ }, step.id))
125191
125349
  })]
125192
125350
  })
125193
125351
  });
@@ -125218,23 +125376,31 @@ function PermissionRequestBubble({ agentId, approval }) {
125218
125376
  setSubmitting(null);
125219
125377
  }
125220
125378
  }
125379
+ const isPlan = approval.kind === "plan";
125221
125380
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125222
125381
  className: "flex justify-start mb-3",
125223
125382
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125224
125383
  className: "max-w-[92%] md:max-w-[78%] rounded-2xl rounded-bl-sm bg-card ring-1 ring-amber-500/35 px-3.5 py-3 text-sm",
125225
125384
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125226
125385
  className: "flex items-start gap-2",
125227
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-400" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125386
+ children: [isPlan ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListChecks, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-400" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-400" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125228
125387
  className: "min-w-0 flex-1",
125229
125388
  children: [
125230
125389
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125231
125390
  className: "text-sm font-medium text-amber-100",
125232
125391
  children: approval.title
125233
125392
  }),
125234
- approval.body && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
125393
+ approval.body && (isPlan ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125394
+ className: "mt-2 max-h-72 overflow-auto rounded-md bg-background/60 p-2.5 text-xs leading-relaxed",
125395
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormattedBody, {
125396
+ text: approval.body,
125397
+ rawBody: approval.body,
125398
+ className: "leading-relaxed"
125399
+ })
125400
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
125235
125401
  className: "mt-2 max-h-44 overflow-auto whitespace-pre-wrap break-words rounded-md bg-background/80 p-2 font-mono text-xs leading-relaxed text-muted-foreground",
125236
125402
  children: approval.body
125237
- }),
125403
+ })),
125238
125404
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125239
125405
  className: "mt-2 flex flex-wrap gap-1.5",
125240
125406
  children: choices.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
@@ -125936,6 +126102,19 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
125936
126102
  })
125937
126103
  });
125938
126104
  });
126105
+ function sessionActivityStep(msg) {
126106
+ if (msg.kind !== "session") return null;
126107
+ const s = msg.payload?.session;
126108
+ if (!s || s.type !== "reasoning" && s.type !== "tool") return null;
126109
+ return {
126110
+ id: msg.id,
126111
+ kind: s.type,
126112
+ label: typeof s.label === "string" ? s.label : void 0,
126113
+ text: msg.body,
126114
+ ts: new Date(msg.createdAt).getTime(),
126115
+ turnId: typeof s.turnId === "string" ? s.turnId : void 0
126116
+ };
126117
+ }
125939
126118
  function dateKey(ts) {
125940
126119
  const d = new Date(ts);
125941
126120
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
@@ -126013,6 +126192,18 @@ function buildTimeline(messages, statusEvents, createdAt, importedHistory = [])
126013
126192
  }
126014
126193
  for (const msg of messages) {
126015
126194
  if (isReactionEvent(msg)) continue;
126195
+ const step = sessionActivityStep(msg);
126196
+ if (step) {
126197
+ raw.push({
126198
+ ts: step.ts,
126199
+ entry: {
126200
+ type: "activity",
126201
+ steps: [step],
126202
+ timestamp: step.ts
126203
+ }
126204
+ });
126205
+ continue;
126206
+ }
126016
126207
  raw.push({
126017
126208
  ts: new Date(msg.createdAt).getTime(),
126018
126209
  entry: {
@@ -126034,6 +126225,7 @@ function buildTimeline(messages, statusEvents, createdAt, importedHistory = [])
126034
126225
  "import-boundary": 2,
126035
126226
  "imported-message": 3,
126036
126227
  message: 4,
126228
+ activity: 4,
126037
126229
  status: 5
126038
126230
  };
126039
126231
  raw.sort((a, b) => {
@@ -126057,10 +126249,23 @@ function buildTimeline(messages, statusEvents, createdAt, importedHistory = [])
126057
126249
  });
126058
126250
  lastDate = dk;
126059
126251
  }
126252
+ if (entry.type === "activity") {
126253
+ const prev = entries[entries.length - 1];
126254
+ if (prev && prev.type === "activity" && sameActivityTurn(prev, entry)) {
126255
+ prev.steps.push(...entry.steps);
126256
+ continue;
126257
+ }
126258
+ }
126060
126259
  entries.push(entry);
126061
126260
  }
126062
126261
  return entries;
126063
126262
  }
126263
+ function sameActivityTurn(a, b) {
126264
+ const at = a.steps[a.steps.length - 1]?.turnId;
126265
+ const bt = b.steps[0]?.turnId;
126266
+ if (at && bt) return at === bt;
126267
+ return !at && !bt;
126268
+ }
126064
126269
  function ChatPanel({ threads, onBack, showBackButton }) {
126065
126270
  const selectedInboxThread = useRelayStore((s) => s.selectedInboxThread);
126066
126271
  const agentsById = useRelayStore((s) => s.agentsById);
@@ -126131,6 +126336,25 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126131
126336
  const canCompact = agentSupportsControlAction(agent, "compact");
126132
126337
  const canClearContext = agentSupportsControlAction(agent, "clearContext");
126133
126338
  const canShutdown = agentSupportsControlAction(agent, "shutdown");
126339
+ const canInterrupt = agentSupportsControlAction(agent, "interrupt");
126340
+ const interruptAgent = () => {
126341
+ if (agent) doAgentAction(agent, "interrupt");
126342
+ };
126343
+ const [slashIndex, setSlashIndex] = (0, import_react.useState)(0);
126344
+ const [slashClosed, setSlashClosed] = (0, import_react.useState)(false);
126345
+ const slashEnabled = agent?.providerCapabilities?.liveSession?.slashCommands === true;
126346
+ const slashProvider = agent?.providerCapabilities?.model?.provider ?? (typeof agent?.meta?.provider === "string" ? agent.meta.provider : void 0);
126347
+ const slashMatches = slashEnabled && !slashClosed && /^\/\S*$/.test(draft) ? providerSlashCommands(slashProvider).filter((c) => c.name.toLowerCase().startsWith(draft.toLowerCase())) : [];
126348
+ const slashActiveIndex = slashMatches.length ? Math.min(slashIndex, slashMatches.length - 1) : 0;
126349
+ function acceptSlash(cmd) {
126350
+ setReplyDraft(selectedInboxThread, `${cmd.name} `);
126351
+ setSlashIndex(0);
126352
+ }
126353
+ function handleDraftChange(value) {
126354
+ setReplyDraft(selectedInboxThread, value);
126355
+ if (slashClosed) setSlashClosed(false);
126356
+ setSlashIndex(0);
126357
+ }
126134
126358
  const hasPendingUploads = pendingAttachments.some((item) => item.uploading);
126135
126359
  const readyAttachments = pendingAttachments.filter((item) => item.artifact);
126136
126360
  useBackNavProtection(draft.trim().length > 0 || pendingAttachments.length > 0, (0, import_react.useCallback)(() => {}, []));
@@ -126320,6 +126544,29 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126320
126544
  clearPendingAttachments();
126321
126545
  }
126322
126546
  function handleKeyDown(e) {
126547
+ if (slashMatches.length) {
126548
+ if (e.key === "ArrowDown") {
126549
+ e.preventDefault();
126550
+ setSlashIndex((i) => Math.min(i + 1, slashMatches.length - 1));
126551
+ return;
126552
+ }
126553
+ if (e.key === "ArrowUp") {
126554
+ e.preventDefault();
126555
+ setSlashIndex((i) => Math.max(i - 1, 0));
126556
+ return;
126557
+ }
126558
+ if (e.key === "Escape") {
126559
+ e.preventDefault();
126560
+ setSlashClosed(true);
126561
+ return;
126562
+ }
126563
+ if (e.key === "Enter" || e.key === "Tab") {
126564
+ e.preventDefault();
126565
+ const cmd = slashMatches[slashActiveIndex];
126566
+ if (cmd) acceptSlash(cmd);
126567
+ return;
126568
+ }
126569
+ }
126323
126570
  if (e.key === "Enter" && !e.shiftKey) {
126324
126571
  e.preventDefault();
126325
126572
  handleSend();
@@ -126406,6 +126653,14 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126406
126653
  onClick: () => void handleOpenTerminal(),
126407
126654
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Terminal, { className: "w-3.5 h-3.5" })
126408
126655
  }),
126656
+ canInterrupt && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126657
+ variant: "ghost",
126658
+ size: "icon-sm",
126659
+ className: "text-red-400 hover:text-red-300",
126660
+ title: "Interrupt",
126661
+ onClick: interruptAgent,
126662
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleStop, { className: "w-3.5 h-3.5" })
126663
+ }),
126409
126664
  canCompact && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126410
126665
  variant: "ghost",
126411
126666
  size: "icon-sm",
@@ -126489,6 +126744,10 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126489
126744
  onClick: () => void handleOpenTerminal(),
126490
126745
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Terminal, { className: "w-3.5 h-3.5" }), "Terminal"]
126491
126746
  }),
126747
+ canInterrupt && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
126748
+ onClick: interruptAgent,
126749
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleStop, { className: "w-3.5 h-3.5" }), "Interrupt"]
126750
+ }),
126492
126751
  canCompact && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, {
126493
126752
  onClick: () => openConfirm("Compact Agent", `Compact context for ${displayName(agent)}?`, () => doAgentAction(agent, "compact")),
126494
126753
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Minimize2, { className: "w-3.5 h-3.5" }), "Compact"]
@@ -126550,6 +126809,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126550
126809
  onPreviewReferencedPath: previewReferencedFile,
126551
126810
  onPreviewReferencedPathEnd: scheduleCloseReferencedFilePreview
126552
126811
  }, entry.msg.id);
126812
+ if (entry.type === "activity") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActivityTrace, { steps: entry.steps }, `act-${entry.steps[0]?.id ?? entry.timestamp}`);
126553
126813
  if (entry.type === "import-boundary") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ImportedHistoryMarker, { history: entry.history }, `import-${entry.history.id}`);
126554
126814
  if (entry.type === "imported-message") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ImportedMessageBubble, {
126555
126815
  entry: entry.entry,
@@ -126564,11 +126824,14 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126564
126824
  agentId: agent.id,
126565
126825
  approval: pendingApproval
126566
126826
  }),
126567
- agent?.status === "busy" && !pendingApproval && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BusyIndicator, { blockedLabel: blockedState?.label })
126827
+ agent?.status === "busy" && !pendingApproval && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BusyIndicator, {
126828
+ blockedLabel: blockedState?.label,
126829
+ onInterrupt: canInterrupt ? interruptAgent : void 0
126830
+ })
126568
126831
  ] })
126569
126832
  }),
126570
126833
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126571
- className: cn$2("shrink-0 border-t border-border px-3 md:px-4 py-2.5 md:py-3 safe-area-bottom", dragActive && "bg-primary/5 ring-1 ring-primary/40"),
126834
+ className: cn$2("relative shrink-0 border-t border-border px-3 md:px-4 py-2.5 md:py-3 safe-area-bottom", dragActive && "bg-primary/5 ring-1 ring-primary/40"),
126572
126835
  onDragOver: (e) => {
126573
126836
  e.preventDefault();
126574
126837
  setDragActive(true);
@@ -126597,6 +126860,31 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126597
126860
  e.currentTarget.value = "";
126598
126861
  }
126599
126862
  }),
126863
+ slashMatches.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
126864
+ className: "absolute bottom-full left-3 right-3 md:left-4 md:right-4 mb-2 z-20 max-h-64 overflow-auto rounded-xl border border-border bg-popover shadow-lg",
126865
+ children: slashMatches.map((cmd, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
126866
+ onMouseDown: (e) => {
126867
+ e.preventDefault();
126868
+ acceptSlash(cmd);
126869
+ },
126870
+ onMouseEnter: () => setSlashIndex(i),
126871
+ className: cn$2("flex w-full items-baseline gap-2 px-3 py-1.5 text-left text-sm", i === slashActiveIndex ? "bg-accent" : "hover:bg-accent/50"),
126872
+ children: [
126873
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
126874
+ className: "font-medium shrink-0",
126875
+ children: cmd.name
126876
+ }),
126877
+ cmd.argumentHint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
126878
+ className: "text-xs text-muted-foreground shrink-0",
126879
+ children: cmd.argumentHint
126880
+ }),
126881
+ cmd.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
126882
+ className: "ml-auto truncate text-xs text-muted-foreground/70",
126883
+ children: cmd.description
126884
+ })
126885
+ ]
126886
+ }, cmd.name))
126887
+ }),
126600
126888
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126601
126889
  className: "hidden md:flex items-end gap-2",
126602
126890
  children: [
@@ -126611,7 +126899,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126611
126899
  }),
126612
126900
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AutoGrowTextarea, {
126613
126901
  value: draft,
126614
- onChange: (e) => setReplyDraft(selectedInboxThread, e.target.value),
126902
+ onChange: (e) => handleDraftChange(e.target.value),
126615
126903
  onKeyDown: handleKeyDown,
126616
126904
  onPaste: handlePaste,
126617
126905
  placeholder: `Message ${agent ? displayName(agent) : selectedInboxThread}…`,
@@ -126630,7 +126918,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126630
126918
  className: "md:hidden space-y-2",
126631
126919
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AutoGrowTextarea, {
126632
126920
  value: draft,
126633
- onChange: (e) => setReplyDraft(selectedInboxThread, e.target.value),
126921
+ onChange: (e) => handleDraftChange(e.target.value),
126634
126922
  onKeyDown: handleKeyDown,
126635
126923
  onPaste: handlePaste,
126636
126924
  placeholder: `Message ${agent ? displayName(agent) : selectedInboxThread}…`,
@@ -157272,6 +157560,10 @@ if ("serviceWorker" in navigator) {
157272
157560
  left: calc(var(--spacing) * 2.5);
157273
157561
  }
157274
157562
 
157563
+ .left-3 {
157564
+ left: calc(var(--spacing) * 3);
157565
+ }
157566
+
157275
157567
  .isolate {
157276
157568
  isolation: isolate;
157277
157569
  }
@@ -157703,6 +157995,10 @@ if ("serviceWorker" in navigator) {
157703
157995
  max-height: calc(var(--spacing) * 44);
157704
157996
  }
157705
157997
 
157998
+ .max-h-64 {
157999
+ max-height: calc(var(--spacing) * 64);
158000
+ }
158001
+
157706
158002
  .max-h-72 {
157707
158003
  max-height: calc(var(--spacing) * 72);
157708
158004
  }
@@ -158015,10 +158311,6 @@ if ("serviceWorker" in navigator) {
158015
158311
  min-width: 980px;
158016
158312
  }
158017
158313
 
158018
- .min-w-full {
158019
- min-width: 100%;
158020
- }
158021
-
158022
158314
  .flex-1 {
158023
158315
  flex: 1;
158024
158316
  }
@@ -158081,6 +158373,10 @@ if ("serviceWorker" in navigator) {
158081
158373
  rotate: 45deg;
158082
158374
  }
158083
158375
 
158376
+ .rotate-90 {
158377
+ rotate: 90deg;
158378
+ }
158379
+
158084
158380
  .rotate-\[-90deg\] {
158085
158381
  rotate: -90deg;
158086
158382
  }
@@ -158157,12 +158453,12 @@ if ("serviceWorker" in navigator) {
158157
158453
  grid-template-columns: 1fr auto;
158158
158454
  }
158159
158455
 
158160
- .grid-cols-\[4\.5rem_minmax\(0\,1fr\)\] {
158161
- grid-template-columns: 4.5rem minmax(0, 1fr);
158456
+ .grid-cols-\[2\.5rem_minmax\(0\,1fr\)\] {
158457
+ grid-template-columns: 2.5rem minmax(0, 1fr);
158162
158458
  }
158163
158459
 
158164
- .grid-cols-\[4rem_minmax\(0\,1fr\)\] {
158165
- grid-template-columns: 4rem minmax(0, 1fr);
158460
+ .grid-cols-\[4\.5rem_minmax\(0\,1fr\)\] {
158461
+ grid-template-columns: 4.5rem minmax(0, 1fr);
158166
158462
  }
158167
158463
 
158168
158464
  .grid-cols-\[minmax\(10rem\,1fr\)_8rem_8rem_7rem_7rem_5rem\] {
@@ -158181,6 +158477,10 @@ if ("serviceWorker" in navigator) {
158181
158477
  flex-wrap: wrap;
158182
158478
  }
158183
158479
 
158480
+ .items-baseline {
158481
+ align-items: baseline;
158482
+ }
158483
+
158184
158484
  .items-center {
158185
158485
  align-items: center;
158186
158486
  }
@@ -158359,10 +158659,6 @@ if ("serviceWorker" in navigator) {
158359
158659
  overflow: hidden;
158360
158660
  }
158361
158661
 
158362
- .overflow-visible {
158363
- overflow: visible;
158364
- }
158365
-
158366
158662
  .overflow-x-auto {
158367
158663
  overflow-x: auto;
158368
158664
  }
@@ -159757,10 +160053,6 @@ if ("serviceWorker" in navigator) {
159757
160053
  padding-right: calc(var(--spacing) * 2);
159758
160054
  }
159759
160055
 
159760
- .pr-3 {
159761
- padding-right: calc(var(--spacing) * 3);
159762
- }
159763
-
159764
160056
  .pr-4 {
159765
160057
  padding-right: calc(var(--spacing) * 4);
159766
160058
  }
@@ -160301,6 +160593,10 @@ if ("serviceWorker" in navigator) {
160301
160593
  text-transform: uppercase;
160302
160594
  }
160303
160595
 
160596
+ .italic {
160597
+ font-style: italic;
160598
+ }
160599
+
160304
160600
  .tabular-nums {
160305
160601
  --tw-numeric-spacing: tabular-nums;
160306
160602
  font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
@@ -160823,6 +161119,16 @@ if ("serviceWorker" in navigator) {
160823
161119
  }
160824
161120
  }
160825
161121
 
161122
+ .hover\:bg-accent\/50:hover {
161123
+ background-color: var(--accent);
161124
+ }
161125
+
161126
+ @supports (color: color-mix(in lab, red, red)) {
161127
+ .hover\:bg-accent\/50:hover {
161128
+ background-color: color-mix(in oklab, var(--accent) 50%, transparent);
161129
+ }
161130
+ }
161131
+
160826
161132
  .hover\:bg-background:hover {
160827
161133
  background-color: var(--background);
160828
161134
  }
@@ -161475,6 +161781,10 @@ if ("serviceWorker" in navigator) {
161475
161781
  grid-template-columns: repeat(3, minmax(0, 1fr));
161476
161782
  }
161477
161783
 
161784
+ .sm\:grid-cols-\[4rem_minmax\(0\,1fr\)\] {
161785
+ grid-template-columns: 4rem minmax(0, 1fr);
161786
+ }
161787
+
161478
161788
  .sm\:flex-row {
161479
161789
  flex-direction: row;
161480
161790
  }
@@ -161483,6 +161793,14 @@ if ("serviceWorker" in navigator) {
161483
161793
  justify-content: flex-end;
161484
161794
  }
161485
161795
 
161796
+ .sm\:px-2 {
161797
+ padding-inline: calc(var(--spacing) * 2);
161798
+ }
161799
+
161800
+ .sm\:pr-3 {
161801
+ padding-right: calc(var(--spacing) * 3);
161802
+ }
161803
+
161486
161804
  .sm\:opacity-0 {
161487
161805
  opacity: 0;
161488
161806
  }
@@ -161495,6 +161813,14 @@ if ("serviceWorker" in navigator) {
161495
161813
  }
161496
161814
 
161497
161815
  @media (min-width: 48rem) {
161816
+ .md\:right-4 {
161817
+ right: calc(var(--spacing) * 4);
161818
+ }
161819
+
161820
+ .md\:left-4 {
161821
+ left: calc(var(--spacing) * 4);
161822
+ }
161823
+
161498
161824
  .md\:-m-6 {
161499
161825
  margin: calc(var(--spacing) * -6);
161500
161826
  }
@@ -161559,6 +161885,15 @@ if ("serviceWorker" in navigator) {
161559
161885
  gap: calc(var(--spacing) * 4);
161560
161886
  }
161561
161887
 
161888
+ .md\:rounded-md {
161889
+ border-radius: calc(var(--radius) * .8);
161890
+ }
161891
+
161892
+ .md\:border {
161893
+ border-style: var(--tw-border-style);
161894
+ border-width: 1px;
161895
+ }
161896
+
161562
161897
  .md\:border-r {
161563
161898
  border-right-style: var(--tw-border-style);
161564
161899
  border-right-width: 1px;
@@ -161569,6 +161904,10 @@ if ("serviceWorker" in navigator) {
161569
161904
  border-bottom-width: 0;
161570
161905
  }
161571
161906
 
161907
+ .md\:border-border {
161908
+ border-color: var(--border);
161909
+ }
161910
+
161572
161911
  .md\:p-6 {
161573
161912
  padding: calc(var(--spacing) * 6);
161574
161913
  }
@@ -27,6 +27,20 @@ export interface ProviderStatusEvent {
27
27
 
28
28
  export type ProviderStatusUpdate = SemanticStatus | ProviderStatusEvent;
29
29
 
30
+ /**
31
+ * A session-mirror event surfaced by an adapter that learns about session
32
+ * activity through provider events rather than hooks/transcripts (e.g. the Codex
33
+ * app-server). The runner turns these into `kind: "session"` chat messages, the
34
+ * same lane Claude's transcript capture uses. Provider-independent boundary.
35
+ */
36
+ export interface ProviderSessionEvent {
37
+ type: "prompt" | "response" | "reasoning" | "tool";
38
+ body: string;
39
+ origin?: "chat" | "terminal" | "provider";
40
+ turnId?: string;
41
+ label?: string;
42
+ }
43
+
30
44
  export interface ProviderConfig {
31
45
  command: string;
32
46
  defaultArgs: string[];
@@ -36,6 +50,9 @@ export interface ProviderConfig {
36
50
  defaultApprovalMode: string;
37
51
  defaultTags: string[];
38
52
  chatCaptureMode: "final" | "full";
53
+ // When false, the runner does not stream reasoning/tool steps into chat. Defaults
54
+ // to enabled (steps render discreetly, never as chat bubbles).
55
+ reasoningCapture?: boolean;
39
56
  headless: {
40
57
  tmuxPrefix: string;
41
58
  shutdownTimeoutMs: number;
@@ -110,11 +127,24 @@ export interface ProviderAdapter {
110
127
  shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void>;
111
128
  compact?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
112
129
  clearContext?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
130
+ // Interrupt the in-flight turn without ending the session (ESC for Claude's
131
+ // tmux pane, turn/interrupt for the Codex app-server). Provider-independent at
132
+ // the runner boundary; each adapter does what its provider actually supports.
133
+ interrupt?(process: ManagedProcess): Promise<Record<string, unknown> | void>;
134
+ // Out-of-band activity probe for the busy-state reconciler: returns the real
135
+ // provider activity when the runner's claim state may have gone stale (e.g. the
136
+ // turn was interrupted from the web terminal so no Stop hook fired). "unknown"
137
+ // means the provider can't be cheaply probed and the reconciler should defer.
138
+ probeActivity?(process: ManagedProcess): Promise<"busy" | "idle" | "unknown">;
113
139
  terminalAttachSpec?(process: ManagedProcess): Promise<TerminalAttachSpec>;
114
140
  respondToPermissionDecision?(process: ManagedProcess, input: ProviderPermissionDecisionInput): Promise<Record<string, unknown> | void>;
115
141
  deliverInitialPrompt?(process: ManagedProcess, prompt: string): Promise<void>;
116
142
  deliver(process: ManagedProcess, messages: Message[]): Promise<void>;
117
143
  onStatusChange(cb: (status: ProviderStatusUpdate) => void): void;
144
+ // Subscribe to session-mirror events from providers that emit them directly
145
+ // (Codex app-server item events). Claude mirrors via hooks/transcript instead,
146
+ // so it leaves this unimplemented.
147
+ onSessionEvent?(cb: (event: ProviderSessionEvent) => void): void;
118
148
  // Headless providers with no tmux session (e.g. the Codex app-server) still
119
149
  // warrant an automatic restart on unexpected exit. Returning true opts the
120
150
  // provider into the runner's restart-with-backoff path.
package/src/routes.ts CHANGED
@@ -320,7 +320,7 @@ const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability
320
320
  const VALID_WORKSPACE_MODES = ["isolated", "shared", "inherit"] as const;
321
321
  const VALID_WORKSPACE_STATUSES = ["active", "ready", "conflict", "review_requested", "merge_planned", "merged", "abandoned", "cleanup_requested", "cleaned"] as const;
322
322
  const VALID_CHANNEL_BINDING_MODES = ["exclusive", "broadcast"] as const;
323
- const VALID_AGENT_ACTIONS = ["restart", "shutdown", "reconnect", "compact", "clearContext", "resume"] as const;
323
+ const VALID_AGENT_ACTIONS = ["restart", "shutdown", "reconnect", "compact", "clearContext", "resume", "interrupt"] as const;
324
324
  const CLAUDE_RESUME_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
325
325
  const VALID_AGENT_SPAWN_PROVIDERS = ["codex"] as const;
326
326
  const VALID_CODEX_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
@@ -2147,11 +2147,12 @@ const deleteAgentById: Handler = (_req, params) => {
2147
2147
 
2148
2148
  type AgentControlAction = (typeof VALID_AGENT_ACTIONS)[number];
2149
2149
 
2150
- function agentControlActionCommandType(action: AgentControlAction): "agent.restart" | "agent.shutdown" | "agent.reconnect" | "agent.compact" | "agent.clearContext" {
2150
+ function agentControlActionCommandType(action: AgentControlAction): "agent.restart" | "agent.shutdown" | "agent.reconnect" | "agent.compact" | "agent.clearContext" | "agent.interrupt" {
2151
2151
  if (action === "restart" || action === "resume") return "agent.restart";
2152
2152
  if (action === "shutdown") return "agent.shutdown";
2153
2153
  if (action === "compact") return "agent.compact";
2154
2154
  if (action === "clearContext") return "agent.clearContext";
2155
+ if (action === "interrupt") return "agent.interrupt";
2155
2156
  return "agent.reconnect";
2156
2157
  }
2157
2158
 
@@ -2161,6 +2162,7 @@ function agentControlActionFromCommandType(type: string): AgentControlAction | n
2161
2162
  if (type === "agent.reconnect") return "reconnect";
2162
2163
  if (type === "agent.compact") return "compact";
2163
2164
  if (type === "agent.clearContext") return "clearContext";
2165
+ if (type === "agent.interrupt") return "interrupt";
2164
2166
  return null;
2165
2167
  }
2166
2168
 
@@ -2170,6 +2172,7 @@ function agentControlActionRequestedTitle(action: AgentControlAction): string {
2170
2172
  if (action === "shutdown") return "Agent shutdown requested";
2171
2173
  if (action === "compact") return "Agent compaction requested";
2172
2174
  if (action === "clearContext") return "Agent context clear requested";
2175
+ if (action === "interrupt") return "Agent interrupt requested";
2173
2176
  return "Agent reconnect requested";
2174
2177
  }
2175
2178
 
@@ -2179,6 +2182,7 @@ function agentControlActionCompletedTitle(action: AgentControlAction): string {
2179
2182
  if (action === "shutdown") return "Agent shut down";
2180
2183
  if (action === "compact") return "Agent compacted";
2181
2184
  if (action === "clearContext") return "Agent context cleared";
2185
+ if (action === "interrupt") return "Agent interrupted";
2182
2186
  return "Agent reconnected";
2183
2187
  }
2184
2188
 
@@ -2187,6 +2191,7 @@ function agentControlActionIcon(action: AgentControlAction): string {
2187
2191
  if (action === "compact") return "ti-compress";
2188
2192
  if (action === "clearContext") return "ti-eraser";
2189
2193
  if (action === "resume") return "ti-player-play";
2194
+ if (action === "interrupt") return "ti-player-stop";
2190
2195
  return "ti-refresh";
2191
2196
  }
2192
2197
 
@@ -2200,6 +2205,9 @@ function agentIsControlEligible(agent: AgentCard): boolean {
2200
2205
  function agentCanReceiveControlAction(agent: AgentCard, action: AgentControlAction): boolean {
2201
2206
  if (!agentIsControlEligible(agent)) return false;
2202
2207
  if (action === "resume") return agentRuntimeProvider(agent) === "claude" && (agent.status === "offline" || agent.status === "stale");
2208
+ // Interrupt only makes sense while the provider is mid-turn, and only if the
2209
+ // provider advertises it can be interrupted from the dashboard.
2210
+ if (action === "interrupt") return agent.providerCapabilities?.liveSession?.interrupt === true && agent.status === "busy";
2203
2211
  const lifecycle = agent.providerCapabilities?.lifecycle;
2204
2212
  if (lifecycle) {
2205
2213
  if (action === "restart") return lifecycle.restartHard === true;