agent-relay-server 0.11.9 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.11.9",
3
+ "version": "0.12.0",
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) {
@@ -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
  }
@@ -158081,6 +158377,10 @@ if ("serviceWorker" in navigator) {
158081
158377
  rotate: 45deg;
158082
158378
  }
158083
158379
 
158380
+ .rotate-90 {
158381
+ rotate: 90deg;
158382
+ }
158383
+
158084
158384
  .rotate-\[-90deg\] {
158085
158385
  rotate: -90deg;
158086
158386
  }
@@ -158181,6 +158481,10 @@ if ("serviceWorker" in navigator) {
158181
158481
  flex-wrap: wrap;
158182
158482
  }
158183
158483
 
158484
+ .items-baseline {
158485
+ align-items: baseline;
158486
+ }
158487
+
158184
158488
  .items-center {
158185
158489
  align-items: center;
158186
158490
  }
@@ -160301,6 +160605,10 @@ if ("serviceWorker" in navigator) {
160301
160605
  text-transform: uppercase;
160302
160606
  }
160303
160607
 
160608
+ .italic {
160609
+ font-style: italic;
160610
+ }
160611
+
160304
160612
  .tabular-nums {
160305
160613
  --tw-numeric-spacing: tabular-nums;
160306
160614
  font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
@@ -160823,6 +161131,16 @@ if ("serviceWorker" in navigator) {
160823
161131
  }
160824
161132
  }
160825
161133
 
161134
+ .hover\:bg-accent\/50:hover {
161135
+ background-color: var(--accent);
161136
+ }
161137
+
161138
+ @supports (color: color-mix(in lab, red, red)) {
161139
+ .hover\:bg-accent\/50:hover {
161140
+ background-color: color-mix(in oklab, var(--accent) 50%, transparent);
161141
+ }
161142
+ }
161143
+
160826
161144
  .hover\:bg-background:hover {
160827
161145
  background-color: var(--background);
160828
161146
  }
@@ -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
  }
@@ -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;