triagent 0.1.0-alpha13 → 0.1.0-alpha18
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 +3 -4
- package/src/cli/config.ts +96 -0
- package/src/index.ts +201 -3
- package/src/integrations/elasticsearch/client.ts +210 -0
- package/src/integrations/grafana/client.ts +186 -0
- package/src/integrations/kubernetes/multi-cluster.ts +199 -0
- package/src/integrations/kubernetes/types.ts +24 -0
- package/src/integrations/loki/client.ts +219 -0
- package/src/integrations/prometheus/client.ts +163 -0
- package/src/integrations/slack/client.ts +265 -0
- package/src/integrations/teams/client.ts +199 -0
- package/src/mastra/agents/debugger.ts +152 -108
- package/src/mastra/tools/approval-store.ts +180 -0
- package/src/mastra/tools/cli.ts +94 -2
- package/src/mastra/tools/cost.ts +389 -0
- package/src/mastra/tools/logs.ts +210 -0
- package/src/mastra/tools/network.ts +253 -0
- package/src/mastra/tools/prometheus.ts +221 -0
- package/src/mastra/tools/remediation.ts +365 -0
- package/src/mastra/tools/runbook.ts +186 -0
- package/src/server/routes/history.ts +207 -0
- package/src/server/routes/notifications.ts +236 -0
- package/src/server/webhook.ts +36 -2
- package/src/storage/index.ts +3 -0
- package/src/storage/investigation-history.ts +277 -0
- package/src/storage/runbook-index.ts +330 -0
- package/src/storage/types.ts +72 -0
- package/src/tui/app.tsx +492 -76
- package/src/tui/components/approval-dialog.tsx +156 -0
- package/src/tui/components/approval-modal.tsx +278 -0
- package/src/tui/components/index.ts +38 -0
- package/src/tui/components/styled-span.tsx +24 -0
- package/src/tui/components/timeline.tsx +223 -0
- package/src/tui/components/toast.tsx +101 -0
package/src/tui/app.tsx
CHANGED
|
@@ -1,22 +1,74 @@
|
|
|
1
1
|
/* @jsxImportSource @opentui/solid */
|
|
2
2
|
import { render } from "@opentui/solid";
|
|
3
|
-
import { createSignal, For, Show, onMount } from "solid-js";
|
|
4
|
-
import { createTextAttributes } from "@opentui/core";
|
|
3
|
+
import { createSignal, For, Show, onMount, type JSX, type Accessor } from "solid-js";
|
|
4
|
+
import { createTextAttributes, SyntaxStyle, type ThemeTokenStyle } from "@opentui/core";
|
|
5
5
|
import "opentui-spinner/solid";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createPulse } from "opentui-spinner";
|
|
7
|
+
import { exec } from "child_process";
|
|
8
|
+
import { promisify } from "util";
|
|
8
9
|
import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
|
|
9
10
|
import type { IncidentInput } from "../mastra/agents/debugger.js";
|
|
11
|
+
import { approvalStore } from "../mastra/tools/approval-store.js";
|
|
12
|
+
import { ToastProvider, toast, toastSuccess, toastError, toastWarning, toastInfo } from "./components/toast.js";
|
|
13
|
+
import { ApprovalDialogProvider, useApprovalDialog, type RiskLevel } from "./components/approval-dialog.js";
|
|
14
|
+
import { StyledSpan } from "./components/styled-span.js";
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
marked.use(markedTerminal() as any);
|
|
16
|
+
const execAsync = promisify(exec);
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
18
|
+
const ATTR_DIM = createTextAttributes({ dim: true });
|
|
19
|
+
|
|
20
|
+
// Markdown syntax highlighting theme (matching opencode's approach)
|
|
21
|
+
const MARKDOWN_SYNTAX_THEME: ThemeTokenStyle[] = [
|
|
22
|
+
// Headings - cyan and bold
|
|
23
|
+
{ scope: ["markup.heading", "markup.heading.1", "markup.heading.2", "markup.heading.3", "markup.heading.4", "markup.heading.5", "markup.heading.6"], style: { foreground: "cyan", bold: true } },
|
|
24
|
+
// Bold/strong - white and bold
|
|
25
|
+
{ scope: ["markup.strong"], style: { foreground: "white", bold: true } },
|
|
26
|
+
// Italic/emphasis - white and italic
|
|
27
|
+
{ scope: ["markup.italic"], style: { foreground: "white", italic: true } },
|
|
28
|
+
// Inline code - yellow
|
|
29
|
+
{ scope: ["markup.raw", "markup.raw.block"], style: { foreground: "yellow" } },
|
|
30
|
+
// Block quotes - gray and italic
|
|
31
|
+
{ scope: ["markup.quote"], style: { foreground: "gray", italic: true } },
|
|
32
|
+
// Lists - gray
|
|
33
|
+
{ scope: ["markup.list", "markup.list.unchecked", "markup.list.checked"], style: { foreground: "gray" } },
|
|
34
|
+
// Links - cyan with underline
|
|
35
|
+
{ scope: ["markup.link", "markup.link.url"], style: { foreground: "cyan", underline: true } },
|
|
36
|
+
{ scope: ["markup.link.label", "markup.link.bracket.close"], style: { foreground: "blue" } },
|
|
37
|
+
// Strikethrough - dim
|
|
38
|
+
{ scope: ["markup.strikethrough"], style: { foreground: "gray", dim: true } },
|
|
39
|
+
// Code block labels (language names)
|
|
40
|
+
{ scope: ["label"], style: { foreground: "gray", dim: true } },
|
|
41
|
+
// Punctuation
|
|
42
|
+
{ scope: ["punctuation.special", "punctuation.delimiter"], style: { foreground: "gray" } },
|
|
43
|
+
// Escape sequences
|
|
44
|
+
{ scope: ["string.escape"], style: { foreground: "magenta" } },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Create the SyntaxStyle instance for markdown
|
|
48
|
+
const markdownSyntaxStyle = SyntaxStyle.fromTheme(MARKDOWN_SYNTAX_THEME);
|
|
49
|
+
|
|
50
|
+
// Pending approval state for HITL
|
|
51
|
+
interface PendingApprovalState {
|
|
52
|
+
approvalId: string;
|
|
53
|
+
command: string;
|
|
54
|
+
riskLevel: "low" | "medium" | "high" | "critical";
|
|
55
|
+
selectedOption: number; // 0 = approve, 1 = reject
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MarkdownText component using opentui's code component with tree-sitter syntax highlighting
|
|
59
|
+
// This matches opencode's approach to markdown rendering
|
|
60
|
+
function MarkdownText(props: { content: string; streaming?: boolean }): JSX.Element {
|
|
61
|
+
return (
|
|
62
|
+
<code
|
|
63
|
+
filetype="markdown"
|
|
64
|
+
content={props.content.trim()}
|
|
65
|
+
syntaxStyle={markdownSyntaxStyle}
|
|
66
|
+
conceal={true}
|
|
67
|
+
drawUnstyledText={false}
|
|
68
|
+
streaming={props.streaming ?? false}
|
|
69
|
+
fg="white"
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
20
72
|
}
|
|
21
73
|
|
|
22
74
|
interface Message {
|
|
@@ -46,11 +98,29 @@ function formatHistoryAsPrompt(history: ConversationMessage[], newMessage: strin
|
|
|
46
98
|
return `Previous conversation:\n${historyText}\n\nUser: ${newMessage}`;
|
|
47
99
|
}
|
|
48
100
|
|
|
49
|
-
type AppStatus = "idle" | "investigating" | "complete" | "error";
|
|
101
|
+
type AppStatus = "idle" | "investigating" | "awaiting_approval" | "complete" | "error";
|
|
50
102
|
|
|
51
|
-
const ATTR_DIM = createTextAttributes({ dim: true });
|
|
52
103
|
const ATTR_BOLD = createTextAttributes({ bold: true });
|
|
53
104
|
|
|
105
|
+
// Risk level color mapping
|
|
106
|
+
function getRiskColor(risk: PendingApprovalState["riskLevel"]): string {
|
|
107
|
+
switch (risk) {
|
|
108
|
+
case "low": return "green";
|
|
109
|
+
case "medium": return "yellow";
|
|
110
|
+
case "high": return "red";
|
|
111
|
+
case "critical": return "magenta";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getRiskEmoji(risk: PendingApprovalState["riskLevel"]): string {
|
|
116
|
+
switch (risk) {
|
|
117
|
+
case "low": return "🟢";
|
|
118
|
+
case "medium": return "🟡";
|
|
119
|
+
case "high": return "🟠";
|
|
120
|
+
case "critical": return "🔴";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
54
124
|
function buildDisplayCommand(toolName: string, args: unknown): string | undefined {
|
|
55
125
|
if (!args || typeof args !== "object") return undefined;
|
|
56
126
|
|
|
@@ -104,6 +174,20 @@ function App() {
|
|
|
104
174
|
const [currentTool, setCurrentTool] = createSignal<string | null>(null);
|
|
105
175
|
const [inputValue, setInputValue] = createSignal("");
|
|
106
176
|
const [error, setError] = createSignal<string | null>(null);
|
|
177
|
+
const [kubeContext, setKubeContext] = createSignal<string>("loading...");
|
|
178
|
+
|
|
179
|
+
// Fetch kubectl context on mount
|
|
180
|
+
onMount(async () => {
|
|
181
|
+
try {
|
|
182
|
+
const { stdout } = await execAsync("kubectl config current-context");
|
|
183
|
+
setKubeContext(stdout.trim());
|
|
184
|
+
} catch {
|
|
185
|
+
setKubeContext("not connected");
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// HITL approval state
|
|
190
|
+
const [pendingApproval, setPendingApproval] = createSignal<PendingApprovalState | null>(null);
|
|
107
191
|
|
|
108
192
|
const addMessage = (msg: Omit<Message, "id" | "timestamp">) => {
|
|
109
193
|
setMessages((prev) => [
|
|
@@ -116,10 +200,166 @@ function App() {
|
|
|
116
200
|
]);
|
|
117
201
|
};
|
|
118
202
|
|
|
203
|
+
// Handle approval selection (Y/N or arrow keys)
|
|
204
|
+
const handleApprovalKey = (key: string) => {
|
|
205
|
+
const approval = pendingApproval();
|
|
206
|
+
if (!approval) return;
|
|
207
|
+
|
|
208
|
+
if (key === "ArrowUp" || key === "ArrowDown") {
|
|
209
|
+
// Toggle selection
|
|
210
|
+
setPendingApproval({ ...approval, selectedOption: approval.selectedOption === 0 ? 1 : 0 });
|
|
211
|
+
} else if (key === "Enter") {
|
|
212
|
+
// Submit selection
|
|
213
|
+
const approved = approval.selectedOption === 0;
|
|
214
|
+
handleApprovalDecision(approved);
|
|
215
|
+
} else if (key === "y" || key === "Y") {
|
|
216
|
+
// Quick approve
|
|
217
|
+
handleApprovalDecision(true);
|
|
218
|
+
} else if (key === "n" || key === "N") {
|
|
219
|
+
// Quick reject
|
|
220
|
+
handleApprovalDecision(false);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleApprovalDecision = async (approved: boolean) => {
|
|
225
|
+
const approval = pendingApproval();
|
|
226
|
+
if (!approval) return;
|
|
227
|
+
|
|
228
|
+
if (approved) {
|
|
229
|
+
// Get approval token from store
|
|
230
|
+
const token = approvalStore.approve(approval.approvalId);
|
|
231
|
+
if (!token) {
|
|
232
|
+
addMessage({
|
|
233
|
+
role: "assistant",
|
|
234
|
+
content: "Approval expired. Please try the operation again.",
|
|
235
|
+
});
|
|
236
|
+
setPendingApproval(null);
|
|
237
|
+
setStatus("complete");
|
|
238
|
+
toastWarning("Approval expired", "Please retry the operation");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add approval message to UI
|
|
243
|
+
addMessage({
|
|
244
|
+
role: "user",
|
|
245
|
+
content: `✓ Approved: ${approval.command}`,
|
|
246
|
+
});
|
|
247
|
+
toastSuccess("Command approved");
|
|
248
|
+
|
|
249
|
+
// Continue the agent with the approval token
|
|
250
|
+
setPendingApproval(null);
|
|
251
|
+
const approvalMessage = `User approved the command. The approval token is: ${token}. Please execute the command: ${approval.command} with approvalToken: "${token}"`;
|
|
252
|
+
|
|
253
|
+
// Add to conversation history
|
|
254
|
+
setConversationHistory((prev) => [
|
|
255
|
+
...prev,
|
|
256
|
+
{ role: "user", content: approvalMessage },
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
// Continue investigation with the approval
|
|
260
|
+
setStatus("investigating");
|
|
261
|
+
continueWithApproval(approvalMessage);
|
|
262
|
+
} else {
|
|
263
|
+
// Reject
|
|
264
|
+
approvalStore.reject(approval.approvalId);
|
|
265
|
+
addMessage({
|
|
266
|
+
role: "user",
|
|
267
|
+
content: `✗ Rejected: ${approval.command}`,
|
|
268
|
+
});
|
|
269
|
+
addMessage({
|
|
270
|
+
role: "assistant",
|
|
271
|
+
content: "Command rejected by user. How would you like to proceed?",
|
|
272
|
+
});
|
|
273
|
+
setPendingApproval(null);
|
|
274
|
+
setStatus("complete");
|
|
275
|
+
toastWarning("Command rejected");
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const continueWithApproval = async (message: string) => {
|
|
280
|
+
try {
|
|
281
|
+
const agent = getDebuggerAgent();
|
|
282
|
+
let assistantContent = "";
|
|
283
|
+
|
|
284
|
+
const prompt = formatHistoryAsPrompt(conversationHistory(), message);
|
|
285
|
+
|
|
286
|
+
const stream = await agent.stream(prompt, {
|
|
287
|
+
maxSteps: 20,
|
|
288
|
+
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
289
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
290
|
+
// Mastra wraps tool calls: toolCall.payload contains the actual data
|
|
291
|
+
const toolCallChunk = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
|
|
292
|
+
const toolName = toolCallChunk?.payload?.toolName ?? "tool";
|
|
293
|
+
const args = toolCallChunk?.payload?.args ?? {};
|
|
294
|
+
|
|
295
|
+
const command = buildDisplayCommand(toolName, args);
|
|
296
|
+
|
|
297
|
+
setCurrentTool(toolName);
|
|
298
|
+
addMessage({
|
|
299
|
+
role: "tool",
|
|
300
|
+
content: command ? `$ ${command}` : `Executing ${toolName}...`,
|
|
301
|
+
toolName,
|
|
302
|
+
command,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check for approval requirement
|
|
307
|
+
if (toolResults && toolResults.length > 0) {
|
|
308
|
+
for (const toolResult of toolResults) {
|
|
309
|
+
// Mastra wraps results: toolResult.payload.result contains the actual data
|
|
310
|
+
const tr = toolResult as any;
|
|
311
|
+
const data = tr?.payload?.result ?? tr?.result ?? tr;
|
|
312
|
+
|
|
313
|
+
if (data?.requiresApproval && data?.approvalId) {
|
|
314
|
+
setPendingApproval({
|
|
315
|
+
approvalId: data.approvalId,
|
|
316
|
+
command: data.command || "unknown command",
|
|
317
|
+
riskLevel: (data.riskLevel as PendingApprovalState["riskLevel"]) || "medium",
|
|
318
|
+
selectedOption: 0, // Default to approve
|
|
319
|
+
});
|
|
320
|
+
setStatus("awaiting_approval");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
for await (const chunk of stream.textStream) {
|
|
328
|
+
assistantContent += chunk;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (status() !== "awaiting_approval") {
|
|
332
|
+
addMessage({
|
|
333
|
+
role: "assistant",
|
|
334
|
+
content: assistantContent,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
setConversationHistory((prev) => [
|
|
338
|
+
...prev,
|
|
339
|
+
{ role: "assistant", content: assistantContent },
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
setStatus("complete");
|
|
343
|
+
toastSuccess("Command executed");
|
|
344
|
+
}
|
|
345
|
+
setCurrentTool(null);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
348
|
+
setError(errorMsg);
|
|
349
|
+
setStatus("error");
|
|
350
|
+
toastError("Execution failed", errorMsg);
|
|
351
|
+
addMessage({
|
|
352
|
+
role: "assistant",
|
|
353
|
+
content: `Error: ${errorMsg}`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
119
358
|
const investigate = async (incident: IncidentInput) => {
|
|
120
359
|
setStatus("investigating");
|
|
121
360
|
setError(null);
|
|
122
361
|
setCurrentTool(null);
|
|
362
|
+
toastInfo("Investigation started", incident.title);
|
|
123
363
|
|
|
124
364
|
// Add user message to UI
|
|
125
365
|
addMessage({
|
|
@@ -150,11 +390,14 @@ function App() {
|
|
|
150
390
|
// Send the formatted prompt to the agent
|
|
151
391
|
const stream = await agent.stream(prompt, {
|
|
152
392
|
maxSteps: 20,
|
|
153
|
-
onStepFinish: (
|
|
393
|
+
onStepFinish: (stepResult) => {
|
|
394
|
+
const { toolCalls, toolResults } = stepResult;
|
|
395
|
+
|
|
154
396
|
if (toolCalls && toolCalls.length > 0) {
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const
|
|
397
|
+
// Mastra wraps tool calls: toolCall.payload contains the actual data
|
|
398
|
+
const toolCallChunk = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
|
|
399
|
+
const toolName = toolCallChunk?.payload?.toolName ?? "tool";
|
|
400
|
+
const args = toolCallChunk?.payload?.args ?? {};
|
|
158
401
|
|
|
159
402
|
// Build display command based on tool type
|
|
160
403
|
const command = buildDisplayCommand(toolName, args);
|
|
@@ -167,6 +410,25 @@ function App() {
|
|
|
167
410
|
command,
|
|
168
411
|
});
|
|
169
412
|
}
|
|
413
|
+
|
|
414
|
+
// Check for approval requirement in tool results
|
|
415
|
+
if (toolResults && toolResults.length > 0) {
|
|
416
|
+
for (const toolResult of toolResults) {
|
|
417
|
+
// Mastra wraps results: toolResult.payload.result contains the actual data
|
|
418
|
+
const tr = toolResult as any;
|
|
419
|
+
const data = tr?.payload?.result ?? tr?.result ?? tr;
|
|
420
|
+
|
|
421
|
+
if (data?.requiresApproval && data?.approvalId) {
|
|
422
|
+
setPendingApproval({
|
|
423
|
+
approvalId: data.approvalId,
|
|
424
|
+
command: data.command || "unknown command",
|
|
425
|
+
riskLevel: (data.riskLevel as PendingApprovalState["riskLevel"]) || "medium",
|
|
426
|
+
selectedOption: 0, // Default to approve
|
|
427
|
+
});
|
|
428
|
+
setStatus("awaiting_approval");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
170
432
|
},
|
|
171
433
|
});
|
|
172
434
|
|
|
@@ -174,24 +436,29 @@ function App() {
|
|
|
174
436
|
assistantContent += chunk;
|
|
175
437
|
}
|
|
176
438
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
439
|
+
// Only add response and set complete if not awaiting approval
|
|
440
|
+
if (status() !== "awaiting_approval") {
|
|
441
|
+
// Add assistant response to UI
|
|
442
|
+
addMessage({
|
|
443
|
+
role: "assistant",
|
|
444
|
+
content: assistantContent,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Add assistant response to conversation history
|
|
448
|
+
setConversationHistory((prev) => [
|
|
449
|
+
...prev,
|
|
450
|
+
{ role: "assistant", content: assistantContent },
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
setStatus("complete");
|
|
454
|
+
toastSuccess("Investigation complete");
|
|
455
|
+
}
|
|
190
456
|
setCurrentTool(null);
|
|
191
457
|
} catch (err) {
|
|
192
458
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
193
459
|
setError(errorMsg);
|
|
194
460
|
setStatus("error");
|
|
461
|
+
toastError("Investigation failed", errorMsg);
|
|
195
462
|
addMessage({
|
|
196
463
|
role: "assistant",
|
|
197
464
|
content: `Error: ${errorMsg}`,
|
|
@@ -219,6 +486,8 @@ function App() {
|
|
|
219
486
|
switch (status()) {
|
|
220
487
|
case "investigating":
|
|
221
488
|
return "yellow";
|
|
489
|
+
case "awaiting_approval":
|
|
490
|
+
return "red";
|
|
222
491
|
case "complete":
|
|
223
492
|
return "green";
|
|
224
493
|
case "error":
|
|
@@ -232,6 +501,8 @@ function App() {
|
|
|
232
501
|
switch (status()) {
|
|
233
502
|
case "investigating":
|
|
234
503
|
return currentTool() ? `Running: ${currentTool()}` : "Investigating...";
|
|
504
|
+
case "awaiting_approval":
|
|
505
|
+
return "Awaiting Approval";
|
|
235
506
|
case "complete":
|
|
236
507
|
return "Complete";
|
|
237
508
|
case "error":
|
|
@@ -249,16 +520,29 @@ function App() {
|
|
|
249
520
|
borderColor="red"
|
|
250
521
|
paddingLeft={2}
|
|
251
522
|
paddingRight={2}
|
|
252
|
-
|
|
253
|
-
|
|
523
|
+
paddingTop={1}
|
|
524
|
+
paddingBottom={1}
|
|
525
|
+
flexDirection="column"
|
|
254
526
|
>
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
527
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
528
|
+
<box flexDirection="row" gap={2}>
|
|
529
|
+
<text fg="red" attributes={ATTR_BOLD}>
|
|
530
|
+
☸
|
|
531
|
+
</text>
|
|
532
|
+
<text fg="red" attributes={ATTR_BOLD}>
|
|
533
|
+
TRIAGENT
|
|
534
|
+
</text>
|
|
535
|
+
</box>
|
|
536
|
+
<text fg={getStatusColor()} attributes={ATTR_BOLD}>
|
|
537
|
+
[{getStatusText()}]
|
|
538
|
+
</text>
|
|
539
|
+
</box>
|
|
540
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
541
|
+
<text fg="gray">Kubernetes Debugging Agent</text>
|
|
542
|
+
<text fg="cyan">
|
|
543
|
+
cluster: {kubeContext()}
|
|
544
|
+
</text>
|
|
545
|
+
</box>
|
|
262
546
|
</box>
|
|
263
547
|
|
|
264
548
|
{/* Messages Area */}
|
|
@@ -296,10 +580,12 @@ function App() {
|
|
|
296
580
|
<Show when={msg.role === "tool"}>
|
|
297
581
|
<box flexDirection="row" gap={1} alignItems="center">
|
|
298
582
|
<Show
|
|
299
|
-
when={status() === "investigating" && msg.id === messages().filter(m => m.role === "tool").at(-1)?.id}
|
|
583
|
+
when={(status() === "investigating" || status() === "awaiting_approval") && msg.id === messages().filter(m => m.role === "tool").at(-1)?.id}
|
|
300
584
|
fallback={<text fg="green">✓</text>}
|
|
301
585
|
>
|
|
302
|
-
<spinner name="dots" color="blue" />
|
|
586
|
+
<Show when={status() === "awaiting_approval"} fallback={<spinner name="dots" color={createPulse(["cyan", "blue", "magenta"], 200)} />}>
|
|
587
|
+
<text fg="yellow">⏸</text>
|
|
588
|
+
</Show>
|
|
303
589
|
</Show>
|
|
304
590
|
<text fg="blue" attributes={ATTR_DIM}>
|
|
305
591
|
[{msg.toolName}]
|
|
@@ -314,47 +600,162 @@ function App() {
|
|
|
314
600
|
<text fg="green" attributes={ATTR_BOLD}>
|
|
315
601
|
Triagent:
|
|
316
602
|
</text>
|
|
317
|
-
<
|
|
318
|
-
{renderMarkdown(msg.content)}
|
|
319
|
-
</text>
|
|
603
|
+
<MarkdownText content={msg.content} />
|
|
320
604
|
</box>
|
|
321
605
|
</Show>
|
|
322
606
|
</box>
|
|
323
607
|
)}
|
|
324
608
|
</For>
|
|
609
|
+
|
|
610
|
+
{/* Approval prompt - Claude Code style */}
|
|
611
|
+
<Show when={pendingApproval()}>
|
|
612
|
+
{(approval: Accessor<PendingApprovalState>) => (
|
|
613
|
+
<box
|
|
614
|
+
flexDirection="column"
|
|
615
|
+
borderStyle="single"
|
|
616
|
+
borderColor={getRiskColor(approval().riskLevel)}
|
|
617
|
+
paddingLeft={1}
|
|
618
|
+
paddingRight={1}
|
|
619
|
+
paddingTop={1}
|
|
620
|
+
paddingBottom={1}
|
|
621
|
+
marginTop={1}
|
|
622
|
+
>
|
|
623
|
+
{/* Header */}
|
|
624
|
+
<box flexDirection="row" gap={1} marginBottom={1}>
|
|
625
|
+
<text fg={getRiskColor(approval().riskLevel)}>
|
|
626
|
+
{getRiskEmoji(approval().riskLevel)}
|
|
627
|
+
</text>
|
|
628
|
+
<text fg="white" attributes={ATTR_BOLD}>
|
|
629
|
+
Write Operation Requires Approval
|
|
630
|
+
</text>
|
|
631
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
632
|
+
({approval().riskLevel} risk)
|
|
633
|
+
</text>
|
|
634
|
+
</box>
|
|
635
|
+
|
|
636
|
+
{/* Command */}
|
|
637
|
+
<box flexDirection="column" marginBottom={1}>
|
|
638
|
+
<text fg="cyan" attributes={ATTR_BOLD}>Command:</text>
|
|
639
|
+
<text fg="yellow">{approval().command}</text>
|
|
640
|
+
</box>
|
|
641
|
+
|
|
642
|
+
{/* Options */}
|
|
643
|
+
<box flexDirection="column" gap={1}>
|
|
644
|
+
<box flexDirection="row" gap={1}>
|
|
645
|
+
<text fg={approval().selectedOption === 0 ? "green" : "gray"}>
|
|
646
|
+
{approval().selectedOption === 0 ? "●" : "○"}
|
|
647
|
+
</text>
|
|
648
|
+
<text
|
|
649
|
+
fg={approval().selectedOption === 0 ? "green" : "white"}
|
|
650
|
+
attributes={approval().selectedOption === 0 ? ATTR_BOLD : undefined}
|
|
651
|
+
>
|
|
652
|
+
Yes, execute this command
|
|
653
|
+
</text>
|
|
654
|
+
</box>
|
|
655
|
+
<box flexDirection="row" gap={1}>
|
|
656
|
+
<text fg={approval().selectedOption === 1 ? "red" : "gray"}>
|
|
657
|
+
{approval().selectedOption === 1 ? "●" : "○"}
|
|
658
|
+
</text>
|
|
659
|
+
<text
|
|
660
|
+
fg={approval().selectedOption === 1 ? "red" : "white"}
|
|
661
|
+
attributes={approval().selectedOption === 1 ? ATTR_BOLD : undefined}
|
|
662
|
+
>
|
|
663
|
+
No, cancel this operation
|
|
664
|
+
</text>
|
|
665
|
+
</box>
|
|
666
|
+
</box>
|
|
667
|
+
|
|
668
|
+
{/* Instructions */}
|
|
669
|
+
<text fg="gray" attributes={ATTR_DIM} marginTop={1}>
|
|
670
|
+
Use ↑↓ to select, Enter to confirm, or press Y/N
|
|
671
|
+
</text>
|
|
672
|
+
</box>
|
|
673
|
+
)}
|
|
674
|
+
</Show>
|
|
325
675
|
</Show>
|
|
326
676
|
</box>
|
|
327
677
|
</scrollbox>
|
|
328
678
|
|
|
329
679
|
{/* Input Area */}
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
680
|
+
<Show
|
|
681
|
+
when={status() === "awaiting_approval"}
|
|
682
|
+
fallback={
|
|
683
|
+
<box
|
|
684
|
+
borderStyle="single"
|
|
685
|
+
borderColor={status() === "investigating" ? "yellow" : "cyan"}
|
|
686
|
+
paddingLeft={1}
|
|
687
|
+
paddingRight={1}
|
|
688
|
+
flexDirection="row"
|
|
689
|
+
gap={1}
|
|
690
|
+
>
|
|
691
|
+
<text fg="cyan" attributes={ATTR_BOLD}>
|
|
692
|
+
{">"}
|
|
693
|
+
</text>
|
|
694
|
+
<input
|
|
695
|
+
flexGrow={1}
|
|
696
|
+
focused={true}
|
|
697
|
+
value={inputValue()}
|
|
698
|
+
onInput={handleInput}
|
|
699
|
+
onSubmit={handleSubmit}
|
|
700
|
+
placeholder={
|
|
701
|
+
status() === "investigating"
|
|
702
|
+
? "Investigating..."
|
|
703
|
+
: "Describe the incident..."
|
|
704
|
+
}
|
|
705
|
+
textColor="white"
|
|
706
|
+
placeholderColor="gray"
|
|
707
|
+
focusedTextColor="white"
|
|
708
|
+
focusedBackgroundColor="#1a1a1a"
|
|
709
|
+
/>
|
|
710
|
+
</box>
|
|
711
|
+
}
|
|
337
712
|
>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
713
|
+
{/* Approval mode input */}
|
|
714
|
+
<box
|
|
715
|
+
borderStyle="single"
|
|
716
|
+
borderColor="red"
|
|
717
|
+
paddingLeft={1}
|
|
718
|
+
paddingRight={1}
|
|
719
|
+
flexDirection="row"
|
|
720
|
+
gap={1}
|
|
721
|
+
>
|
|
722
|
+
<text fg="red" attributes={ATTR_BOLD}>
|
|
723
|
+
?
|
|
724
|
+
</text>
|
|
725
|
+
<input
|
|
726
|
+
flexGrow={1}
|
|
727
|
+
focused={true}
|
|
728
|
+
value=""
|
|
729
|
+
onInput={(value) => {
|
|
730
|
+
// Handle Y/N keys
|
|
731
|
+
if (value.toLowerCase() === "y") {
|
|
732
|
+
handleApprovalDecision(true);
|
|
733
|
+
} else if (value.toLowerCase() === "n") {
|
|
734
|
+
handleApprovalDecision(false);
|
|
735
|
+
}
|
|
736
|
+
}}
|
|
737
|
+
onKeyDown={(key) => {
|
|
738
|
+
// KeyEvent has a 'name' property
|
|
739
|
+
if (key.name === "up" || key.name === "down") {
|
|
740
|
+
const approval = pendingApproval();
|
|
741
|
+
if (approval) {
|
|
742
|
+
setPendingApproval({ ...approval, selectedOption: approval.selectedOption === 0 ? 1 : 0 });
|
|
743
|
+
}
|
|
744
|
+
} else if (key.name === "return") {
|
|
745
|
+
const approval = pendingApproval();
|
|
746
|
+
if (approval) {
|
|
747
|
+
handleApprovalDecision(approval.selectedOption === 0);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}}
|
|
751
|
+
placeholder="Press Y to approve, N to reject, or use ↑↓ and Enter"
|
|
752
|
+
textColor="white"
|
|
753
|
+
placeholderColor="gray"
|
|
754
|
+
focusedTextColor="white"
|
|
755
|
+
focusedBackgroundColor="#1a1a1a"
|
|
756
|
+
/>
|
|
757
|
+
</box>
|
|
758
|
+
</Show>
|
|
358
759
|
|
|
359
760
|
{/* Footer */}
|
|
360
761
|
<box
|
|
@@ -363,9 +764,18 @@ function App() {
|
|
|
363
764
|
flexDirection="row"
|
|
364
765
|
justifyContent="space-between"
|
|
365
766
|
>
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
767
|
+
<Show
|
|
768
|
+
when={status() === "awaiting_approval"}
|
|
769
|
+
fallback={
|
|
770
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
771
|
+
Press Enter to submit | Ctrl+C to quit
|
|
772
|
+
</text>
|
|
773
|
+
}
|
|
774
|
+
>
|
|
775
|
+
<text fg="yellow" attributes={ATTR_BOLD}>
|
|
776
|
+
⚠ Approval required: Y/N or ↑↓ + Enter
|
|
777
|
+
</text>
|
|
778
|
+
</Show>
|
|
369
779
|
<text fg="gray" attributes={ATTR_DIM}>
|
|
370
780
|
{messages().length} messages
|
|
371
781
|
</text>
|
|
@@ -380,7 +790,13 @@ export interface TUIHandle {
|
|
|
380
790
|
}
|
|
381
791
|
|
|
382
792
|
export async function runTUI(): Promise<TUIHandle> {
|
|
383
|
-
await render(() =>
|
|
793
|
+
await render(() => (
|
|
794
|
+
<ToastProvider>
|
|
795
|
+
<ApprovalDialogProvider>
|
|
796
|
+
<App />
|
|
797
|
+
</ApprovalDialogProvider>
|
|
798
|
+
</ToastProvider>
|
|
799
|
+
));
|
|
384
800
|
|
|
385
801
|
return {
|
|
386
802
|
shutdown: () => {
|