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 +2 -2
- package/public/index.html +342 -16
- package/runner/src/adapter.ts +30 -0
- package/src/routes.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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: [
|
|
125182
|
-
|
|
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)(
|
|
125185
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
125186
|
-
|
|
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)("
|
|
125189
|
-
className: "
|
|
125190
|
-
children: "
|
|
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)("
|
|
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, {
|
|
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) =>
|
|
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) =>
|
|
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
|
}
|
package/runner/src/adapter.ts
CHANGED
|
@@ -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;
|