@chrysb/alphaclaw 0.8.1-beta.0 → 0.8.1-beta.2

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.
@@ -1,6 +1,6 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import htm from "https://esm.sh/htm";
3
- import { Webhooks } from "../webhooks.js";
3
+ import { Webhooks } from "../webhooks/index.js";
4
4
 
5
5
  const html = htm.bind(h);
6
6
 
@@ -0,0 +1,176 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import {
4
+ kNoDestinationSessionValue,
5
+ useDestinationSessionSelection,
6
+ } from "../../../hooks/use-destination-session-selection.js";
7
+ import { ActionButton } from "../../action-button.js";
8
+ import { CloseIcon } from "../../icons.js";
9
+ import { ModalShell } from "../../modal-shell.js";
10
+ import { PageHeader } from "../../page-header.js";
11
+ import { SessionSelectField } from "../../session-select-field.js";
12
+
13
+ const html = htm.bind(h);
14
+
15
+ export const CreateWebhookModal = ({
16
+ visible,
17
+ name,
18
+ mode = "webhook",
19
+ onModeChange = () => {},
20
+ onNameChange = () => {},
21
+ canCreate = false,
22
+ creating = false,
23
+ onCreate = () => {},
24
+ onClose = () => {},
25
+ }) => {
26
+ const {
27
+ sessions: selectableSessions,
28
+ loading: loadingSessions,
29
+ error: destinationLoadError,
30
+ destinationSessionKey,
31
+ setDestinationSessionKey,
32
+ selectedDestination,
33
+ } = useDestinationSessionSelection({
34
+ enabled: visible,
35
+ resetKey: String(visible),
36
+ });
37
+
38
+ const normalized = String(name || "")
39
+ .trim()
40
+ .toLowerCase();
41
+ const previewName = normalized || "{name}";
42
+ const previewPath = `/hooks/${previewName}`;
43
+ const previewUrl =
44
+ mode === "oauth"
45
+ ? `${window.location.origin}/oauth/{id}`
46
+ : `${window.location.origin}${previewPath}`;
47
+ if (!visible) return null;
48
+
49
+ return html`
50
+ <${ModalShell}
51
+ visible=${visible}
52
+ onClose=${onClose}
53
+ panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
54
+ >
55
+ <${PageHeader}
56
+ title="Create Webhook"
57
+ actions=${html`
58
+ <button
59
+ type="button"
60
+ onclick=${onClose}
61
+ class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
62
+ aria-label="Close modal"
63
+ >
64
+ <${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
65
+ </button>
66
+ `}
67
+ />
68
+ <div class="space-y-2">
69
+ <p class="text-xs text-gray-500">Endpoint mode</p>
70
+ <div class="flex items-center gap-2">
71
+ <button
72
+ class="text-xs px-2 py-1 rounded border transition-colors ${mode ===
73
+ "webhook"
74
+ ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
75
+ : "border-border text-gray-400 hover:text-gray-200"}"
76
+ onclick=${() => onModeChange("webhook")}
77
+ >
78
+ Webhook
79
+ </button>
80
+ <button
81
+ class="text-xs px-2 py-1 rounded border transition-colors ${mode ===
82
+ "oauth"
83
+ ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
84
+ : "border-border text-gray-400 hover:text-gray-200"}"
85
+ onclick=${() => onModeChange("oauth")}
86
+ >
87
+ OAuth Callback
88
+ </button>
89
+ </div>
90
+ </div>
91
+ <div class="space-y-2">
92
+ <p class="text-xs text-gray-500">Name</p>
93
+ <input
94
+ type="text"
95
+ value=${name}
96
+ placeholder="fathom"
97
+ onInput=${(e) => onNameChange(e.target.value)}
98
+ onKeyDown=${(e) => {
99
+ if (e.key === "Enter" && canCreate && !creating) {
100
+ onCreate(selectedDestination, mode);
101
+ }
102
+ if (e.key === "Escape") onClose();
103
+ }}
104
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
105
+ />
106
+ </div>
107
+ <${SessionSelectField}
108
+ label="Deliver to"
109
+ sessions=${selectableSessions}
110
+ selectedSessionKey=${destinationSessionKey}
111
+ onChangeSessionKey=${setDestinationSessionKey}
112
+ disabled=${loadingSessions || creating}
113
+ loading=${loadingSessions}
114
+ error=${destinationLoadError}
115
+ allowNone=${true}
116
+ noneValue=${kNoDestinationSessionValue}
117
+ noneLabel="Default"
118
+ emptyStateText="No paired chat sessions found yet. You can still create the webhook without a default destination."
119
+ loadingLabel="Loading destinations..."
120
+ />
121
+ <div class="border border-border rounded-lg overflow-hidden">
122
+ <table class="w-full text-xs">
123
+ <tbody>
124
+ <tr class="border-b border-border">
125
+ <td class="w-24 px-3 py-2 text-gray-500">Path</td>
126
+ <td class="px-3 py-2 text-gray-300 font-mono">
127
+ <code>${previewPath}</code>
128
+ </td>
129
+ </tr>
130
+ <tr class="border-b border-border">
131
+ <td class="w-24 px-3 py-2 text-gray-500">URL</td>
132
+ <td class="px-3 py-2 text-gray-300 font-mono break-all">
133
+ <code>${previewUrl}</code>
134
+ </td>
135
+ </tr>
136
+ <tr>
137
+ <td class="w-24 px-3 py-2 text-gray-500">Transform</td>
138
+ <td class="px-3 py-2 text-gray-300 font-mono">
139
+ <code>hooks/transforms/${previewName}/${previewName}-transform.mjs</code>
140
+ </td>
141
+ </tr>
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ ${mode === "oauth"
146
+ ? html`
147
+ <div class="space-y-1">
148
+ <p class="text-xs text-gray-500">
149
+ For OAuth providers that can't send auth headers. AlphaClaw
150
+ injects webhook auth before forwarding to /hooks/{name}.
151
+ </p>
152
+ </div>
153
+ `
154
+ : null}
155
+ <div class="pt-1 flex items-center justify-end gap-2">
156
+ <${ActionButton}
157
+ onClick=${onClose}
158
+ tone="secondary"
159
+ size="md"
160
+ idleLabel="Cancel"
161
+ className="px-4 py-2 rounded-lg text-sm"
162
+ />
163
+ <${ActionButton}
164
+ onClick=${() => onCreate(selectedDestination, mode)}
165
+ disabled=${!canCreate || creating}
166
+ loading=${creating}
167
+ tone="primary"
168
+ size="md"
169
+ idleLabel="Create"
170
+ loadingLabel="Creating..."
171
+ className="px-4 py-2 rounded-lg text-sm"
172
+ />
173
+ </div>
174
+ </${ModalShell}>
175
+ `;
176
+ };
@@ -0,0 +1,106 @@
1
+ import {
2
+ formatLocaleDateTime,
3
+ formatLocaleDateTimeWithTodayTime,
4
+ } from "../../lib/format.js";
5
+
6
+ export const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7
+ export const kStatusFilters = ["all", "success", "error"];
8
+
9
+ export const formatDateTime = (value) => {
10
+ return formatLocaleDateTime(value, { fallback: "—" });
11
+ };
12
+
13
+ export const formatLastReceived = (value) => {
14
+ return formatLocaleDateTimeWithTodayTime(value, { fallback: "—" });
15
+ };
16
+
17
+ export const formatBytes = (size) => {
18
+ const bytes = Number(size || 0);
19
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0B";
20
+ if (bytes < 1024) return `${bytes}B`;
21
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
22
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
23
+ };
24
+
25
+ export const healthClassName = (health) => {
26
+ if (health === "red") return "bg-red-500";
27
+ if (health === "yellow") return "bg-yellow-500";
28
+ return "bg-green-500";
29
+ };
30
+
31
+ export const getRequestStatusTone = (status) => {
32
+ if (status === "success") {
33
+ return {
34
+ dotClass: "bg-green-500/90",
35
+ textClass: "text-green-500/90",
36
+ };
37
+ }
38
+ if (status === "error") {
39
+ return {
40
+ dotClass: "bg-red-500/90",
41
+ textClass: "text-red-500/90",
42
+ };
43
+ }
44
+ return {
45
+ dotClass: "bg-gray-500/70",
46
+ textClass: "text-gray-400",
47
+ };
48
+ };
49
+
50
+ export const formatAgentFallbackName = (agentId = "") =>
51
+ String(agentId || "")
52
+ .trim()
53
+ .split(/[-_\s]+/)
54
+ .filter(Boolean)
55
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
56
+ .join(" ") || "Main Agent";
57
+
58
+ export const jsonPretty = (value) => {
59
+ if (typeof value === "string") {
60
+ try {
61
+ const parsed = JSON.parse(value);
62
+ return JSON.stringify(parsed, null, 2);
63
+ } catch {
64
+ return value;
65
+ }
66
+ }
67
+ try {
68
+ return JSON.stringify(value || {}, null, 2);
69
+ } catch {
70
+ return String(value || "");
71
+ }
72
+ };
73
+
74
+ export const buildWebhookDebugMessage = ({
75
+ hookName = "",
76
+ webhook = null,
77
+ request = null,
78
+ }) => {
79
+ const hookPath =
80
+ String(webhook?.path || "").trim() ||
81
+ (hookName ? `/hooks/${hookName}` : "/hooks/unknown");
82
+ const gatewayStatus =
83
+ request?.gatewayStatus == null ? "n/a" : String(request.gatewayStatus);
84
+ return [
85
+ "Investigate this failed webhook request and share findings before fixing anything.",
86
+ "Reply with your diagnosis first, including the likely root cause, any relevant risks, and what you would change if I approve a fix.",
87
+ "",
88
+ `Webhook: ${hookPath}`,
89
+ `Request ID: ${String(request?.id || "unknown")}`,
90
+ `Time: ${String(request?.createdAt || "unknown")}`,
91
+ `Method: ${String(request?.method || "unknown")}`,
92
+ `Source IP: ${String(request?.sourceIp || "unknown")}`,
93
+ `Gateway status: ${gatewayStatus}`,
94
+ `Transform path: ${String(webhook?.transformPath || "unknown")}`,
95
+ `Payload truncated: ${request?.payloadTruncated ? "yes" : "no"}`,
96
+ "",
97
+ "Headers:",
98
+ jsonPretty(request?.headers),
99
+ "",
100
+ "Payload:",
101
+ jsonPretty(request?.payload),
102
+ "",
103
+ "Gateway response:",
104
+ jsonPretty(request?.gatewayBody),
105
+ ].join("\n");
106
+ };
@@ -0,0 +1,148 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { createWebhook } from "../../lib/api.js";
5
+ import { ActionButton } from "../action-button.js";
6
+ import { PageHeader } from "../page-header.js";
7
+ import { showToast } from "../toast.js";
8
+ import { CreateWebhookModal } from "./create-webhook-modal/index.js";
9
+ import { kNamePattern } from "./helpers.js";
10
+ import { WebhookDetail } from "./webhook-detail/index.js";
11
+ import { WebhookList } from "./webhook-list/index.js";
12
+
13
+ const html = htm.bind(h);
14
+
15
+ export const Webhooks = ({
16
+ selectedHookName = "",
17
+ onSelectHook = () => {},
18
+ onBackToList = () => {},
19
+ onRestartRequired = () => {},
20
+ onOpenFile = () => {},
21
+ }) => {
22
+ const [isCreating, setIsCreating] = useState(false);
23
+ const [newName, setNewName] = useState("");
24
+ const [createMode, setCreateMode] = useState("webhook");
25
+ const [creating, setCreating] = useState(false);
26
+
27
+ const canCreate = useMemo(() => {
28
+ const name = String(newName || "")
29
+ .trim()
30
+ .toLowerCase();
31
+ return kNamePattern.test(name);
32
+ }, [newName]);
33
+
34
+ const handleCreate = useCallback(
35
+ async (destination = null, mode = "webhook") => {
36
+ const candidateName = String(newName || "")
37
+ .trim()
38
+ .toLowerCase();
39
+ if (!kNamePattern.test(candidateName)) {
40
+ showToast(
41
+ "Name must be lowercase letters, numbers, and hyphens",
42
+ "error",
43
+ );
44
+ return;
45
+ }
46
+ if (creating) return;
47
+ setCreating(true);
48
+ try {
49
+ const data = await createWebhook(candidateName, {
50
+ destination,
51
+ oauthCallback: mode === "oauth",
52
+ });
53
+ setIsCreating(false);
54
+ setNewName("");
55
+ setCreateMode("webhook");
56
+ onSelectHook(candidateName);
57
+ if (data.restartRequired) onRestartRequired(true);
58
+ if (mode === "oauth" && data?.webhook?.oauthCallbackUrl) {
59
+ showToast("Webhook + OAuth callback created", "success");
60
+ } else {
61
+ showToast("Webhook created", "success");
62
+ }
63
+ if (data.syncWarning) {
64
+ showToast(`Created, but git-sync failed: ${data.syncWarning}`, "warning");
65
+ }
66
+ } catch (err) {
67
+ showToast(err.message || "Could not create webhook", "error");
68
+ } finally {
69
+ setCreating(false);
70
+ }
71
+ },
72
+ [creating, newName, onRestartRequired, onSelectHook],
73
+ );
74
+
75
+ return html`
76
+ <div class="space-y-4">
77
+ <${PageHeader}
78
+ title="Webhooks"
79
+ leading=${selectedHookName
80
+ ? html`
81
+ <button
82
+ class="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-300 transition-colors"
83
+ onclick=${onBackToList}
84
+ >
85
+ <svg
86
+ width="16"
87
+ height="16"
88
+ viewBox="0 0 16 16"
89
+ fill="currentColor"
90
+ >
91
+ <path
92
+ d="M10.354 3.354a.5.5 0 00-.708-.708l-5 5a.5.5 0 000 .708l5 5a.5.5 0 00.708-.708L5.707 8l4.647-4.646z"
93
+ />
94
+ </svg>
95
+ Back
96
+ </button>
97
+ `
98
+ : null}
99
+ actions=${selectedHookName
100
+ ? null
101
+ : html`
102
+ <${ActionButton}
103
+ onClick=${() => {
104
+ setCreateMode("webhook");
105
+ setIsCreating((open) => !open);
106
+ }}
107
+ tone="secondary"
108
+ size="sm"
109
+ idleLabel="Create new"
110
+ className="px-3 py-1.5"
111
+ />
112
+ `}
113
+ />
114
+
115
+ ${selectedHookName
116
+ ? html`
117
+ <${WebhookDetail}
118
+ selectedHookName=${selectedHookName}
119
+ onBackToList=${onBackToList}
120
+ onRestartRequired=${onRestartRequired}
121
+ onOpenFile=${onOpenFile}
122
+ />
123
+ `
124
+ : html`
125
+ <${WebhookList}
126
+ onSelectHook=${(name) => {
127
+ onSelectHook(name);
128
+ }}
129
+ />
130
+ `}
131
+
132
+ <${CreateWebhookModal}
133
+ visible=${isCreating && !selectedHookName}
134
+ name=${newName}
135
+ mode=${createMode}
136
+ onModeChange=${setCreateMode}
137
+ onNameChange=${setNewName}
138
+ canCreate=${canCreate}
139
+ creating=${creating}
140
+ onCreate=${handleCreate}
141
+ onClose=${() => {
142
+ setIsCreating(false);
143
+ setCreateMode("webhook");
144
+ }}
145
+ />
146
+ </div>
147
+ `;
148
+ };
@@ -0,0 +1,241 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useMemo } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { sendAgentMessage } from "../../../lib/api.js";
5
+ import { ActionButton } from "../../action-button.js";
6
+ import { AgentSendModal } from "../../agent-send-modal.js";
7
+ import { showToast } from "../../toast.js";
8
+ import {
9
+ buildWebhookDebugMessage,
10
+ formatBytes,
11
+ formatLastReceived,
12
+ getRequestStatusTone,
13
+ jsonPretty,
14
+ kStatusFilters,
15
+ } from "../helpers.js";
16
+ import { useRequestHistory } from "./use-request-history.js";
17
+
18
+ const html = htm.bind(h);
19
+
20
+ export const RequestHistory = ({
21
+ selectedHookName = "",
22
+ effectiveAuthMode = "headers",
23
+ webhookUrl = "",
24
+ webhookUrlWithQueryToken = "",
25
+ bearerTokenValue = "",
26
+ selectedWebhook = null,
27
+ }) => {
28
+ const { state, actions } = useRequestHistory({
29
+ selectedHookName,
30
+ effectiveAuthMode,
31
+ webhookUrl,
32
+ webhookUrlWithQueryToken,
33
+ bearerTokenValue,
34
+ });
35
+
36
+ const {
37
+ requests,
38
+ statusFilter,
39
+ expandedRows,
40
+ replayingRequestId,
41
+ debugLoadingRequestId,
42
+ debugRequest,
43
+ } = state;
44
+
45
+ const debugAgentMessage = useMemo(
46
+ () =>
47
+ buildWebhookDebugMessage({
48
+ hookName: selectedHookName,
49
+ webhook: selectedWebhook,
50
+ request: debugRequest,
51
+ }),
52
+ [debugRequest, selectedHookName, selectedWebhook],
53
+ );
54
+
55
+ return html`
56
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
57
+ <div class="flex items-center justify-between gap-3">
58
+ <h3 class="card-label">Request history</h3>
59
+ <div class="flex items-center gap-2">
60
+ ${kStatusFilters.map(
61
+ (filter) => html`
62
+ <button
63
+ class="text-xs px-2 py-1 rounded border ${statusFilter === filter
64
+ ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
65
+ : "border-border text-gray-400 hover:text-gray-200"}"
66
+ onclick=${() => actions.handleSetStatusFilter(filter)}
67
+ >
68
+ ${filter}
69
+ </button>
70
+ `,
71
+ )}
72
+ </div>
73
+ </div>
74
+
75
+ ${requests.length === 0
76
+ ? html`<p class="text-sm text-gray-500">No requests logged yet.</p>`
77
+ : html`
78
+ <div class="ac-history-list">
79
+ ${requests.map((item) => {
80
+ const statusTone = getRequestStatusTone(item.status);
81
+ return html`
82
+ <details
83
+ class="ac-history-item"
84
+ open=${expandedRows.has(item.id)}
85
+ ontoggle=${(e) =>
86
+ actions.handleRequestRowToggle(item.id, !!e.currentTarget?.open)}
87
+ >
88
+ <summary class="ac-history-summary">
89
+ <div class="ac-history-summary-row">
90
+ <span class="inline-flex items-center gap-2 min-w-0">
91
+ <span class="ac-history-toggle shrink-0" aria-hidden="true"
92
+ >▸</span
93
+ >
94
+ <span class="truncate text-xs text-gray-300">
95
+ ${formatLastReceived(item.createdAt)}
96
+ </span>
97
+ </span>
98
+ <span class="inline-flex items-center gap-2 shrink-0">
99
+ <span class="text-xs text-gray-500"
100
+ >${formatBytes(item.payloadSize)}</span
101
+ >
102
+ <span class=${`text-xs font-medium ${statusTone.textClass}`}
103
+ >${item.gatewayStatus || "n/a"}</span
104
+ >
105
+ <span class="inline-flex items-center">
106
+ <span
107
+ class=${`h-2.5 w-2.5 rounded-full ${statusTone.dotClass}`}
108
+ title=${item.status || "unknown"}
109
+ aria-label=${item.status || "unknown"}
110
+ ></span>
111
+ </span>
112
+ </span>
113
+ </div>
114
+ </summary>
115
+ ${expandedRows.has(item.id)
116
+ ? html`
117
+ <div class="ac-history-body space-y-3">
118
+ <div>
119
+ <p class="text-[11px] text-gray-500 mb-1">Headers</p>
120
+ <pre
121
+ class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
122
+ >
123
+ ${jsonPretty(item.headers)}</pre
124
+ >
125
+ <div class="mt-2 flex justify-start gap-2">
126
+ <button
127
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
128
+ onclick=${() =>
129
+ actions.handleCopyRequestField(
130
+ jsonPretty(item.headers),
131
+ "Headers",
132
+ )}
133
+ >
134
+ Copy
135
+ </button>
136
+ </div>
137
+ </div>
138
+ <div>
139
+ <p class="text-[11px] text-gray-500 mb-1">
140
+ Payload ${item.payloadTruncated ? "(truncated)" : ""}
141
+ </p>
142
+ <pre
143
+ class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
144
+ >
145
+ ${jsonPretty(item.payload)}</pre
146
+ >
147
+ <div class="mt-2 flex justify-start gap-2">
148
+ <button
149
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
150
+ onclick=${() =>
151
+ actions.handleCopyRequestField(
152
+ item.payload,
153
+ "Payload",
154
+ )}
155
+ >
156
+ Copy
157
+ </button>
158
+ <button
159
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
160
+ onclick=${() => actions.handleReplayRequest(item)}
161
+ disabled=${item.payloadTruncated ||
162
+ replayingRequestId === item.id}
163
+ title=${item.payloadTruncated
164
+ ? "Cannot replay truncated payload"
165
+ : "Replay this payload"}
166
+ >
167
+ ${replayingRequestId === item.id
168
+ ? "Replaying..."
169
+ : "Replay"}
170
+ </button>
171
+ </div>
172
+ </div>
173
+ <div>
174
+ <p class="text-[11px] text-gray-500 mb-1">
175
+ Gateway response (${item.gatewayStatus || "n/a"})
176
+ </p>
177
+ <pre
178
+ class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
179
+ >
180
+ ${jsonPretty(item.gatewayBody)}</pre
181
+ >
182
+ <div class="mt-2 flex justify-start gap-2">
183
+ <button
184
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
185
+ onclick=${() =>
186
+ actions.handleCopyRequestField(
187
+ item.gatewayBody,
188
+ "Gateway response",
189
+ )}
190
+ >
191
+ Copy
192
+ </button>
193
+ ${item.status === "error"
194
+ ? html`<${ActionButton}
195
+ onClick=${() =>
196
+ actions.handleAskAgentToDebug(item)}
197
+ loading=${debugLoadingRequestId === item.id}
198
+ tone="primary"
199
+ size="sm"
200
+ idleLabel="Ask agent to debug"
201
+ loadingLabel="Loading..."
202
+ className="h-7 px-2.5"
203
+ />`
204
+ : null}
205
+ </div>
206
+ </div>
207
+ </div>
208
+ `
209
+ : null}
210
+ </details>
211
+ `;
212
+ })}
213
+ </div>
214
+ `}
215
+ <${AgentSendModal}
216
+ visible=${!!debugRequest}
217
+ title="Ask agent to debug"
218
+ messageLabel="Debug request"
219
+ messageRows=${12}
220
+ initialMessage=${debugAgentMessage}
221
+ resetKey=${String(debugRequest?.id || "")}
222
+ submitLabel="Send debug request"
223
+ loadingLabel="Sending..."
224
+ onClose=${() => actions.setDebugRequest(null)}
225
+ onSubmit=${async ({ selectedSessionKey, message }) => {
226
+ try {
227
+ await sendAgentMessage({
228
+ message,
229
+ sessionKey: selectedSessionKey,
230
+ });
231
+ showToast("Debug request sent to agent", "success");
232
+ return true;
233
+ } catch (err) {
234
+ showToast(err.message || "Could not send debug request", "error");
235
+ return false;
236
+ }
237
+ }}
238
+ />
239
+ </div>
240
+ `;
241
+ };