triagent 0.1.0-alpha8 → 0.1.0-beta2
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/README.md +101 -1
- package/package.json +9 -3
- package/src/cli/config.ts +118 -2
- package/src/config.ts +23 -3
- package/src/index.ts +262 -6
- 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 +164 -109
- package/src/mastra/index.ts +2 -2
- 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/sandbox/bashlet.ts +76 -10
- 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 +278 -198
- package/src/tui/components/approval-dialog.tsx +147 -0
- package/src/tui/components/approval-modal.tsx +278 -0
- package/src/tui/components/centered-layout.tsx +33 -0
- package/src/tui/components/editor.tsx +87 -0
- package/src/tui/components/header.tsx +53 -0
- package/src/tui/components/index.ts +55 -0
- package/src/tui/components/message-item.tsx +131 -0
- package/src/tui/components/messages-panel.tsx +71 -0
- package/src/tui/components/status-badge.tsx +20 -0
- package/src/tui/components/status-bar.tsx +39 -0
- package/src/tui/components/styled-span.tsx +24 -0
- package/src/tui/components/timeline.tsx +223 -0
- package/src/tui/components/toast.tsx +104 -0
- package/src/tui/theme/index.ts +21 -0
- package/src/tui/theme/tokens.ts +180 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Show } from "solid-js";
|
|
3
|
+
import type { JSX } from "solid-js";
|
|
4
|
+
import { SyntaxStyle, type ThemeTokenStyle } from "@opentui/core";
|
|
5
|
+
import "opentui-spinner/solid";
|
|
6
|
+
import { createPulse } from "opentui-spinner";
|
|
7
|
+
import { colors, spacing, ATTR_BOLD, ATTR_DIM, ATTR_ITALIC, type AppStatus } from "../theme/index.js";
|
|
8
|
+
|
|
9
|
+
// Markdown syntax highlighting theme (matching opencode's approach)
|
|
10
|
+
const MARKDOWN_SYNTAX_THEME: ThemeTokenStyle[] = [
|
|
11
|
+
{ 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 } },
|
|
12
|
+
{ scope: ["markup.strong"], style: { foreground: "white", bold: true } },
|
|
13
|
+
{ scope: ["markup.italic"], style: { foreground: "white", italic: true } },
|
|
14
|
+
{ scope: ["markup.raw", "markup.raw.block"], style: { foreground: "yellow" } },
|
|
15
|
+
{ scope: ["markup.quote"], style: { foreground: "gray", italic: true } },
|
|
16
|
+
{ scope: ["markup.list", "markup.list.unchecked", "markup.list.checked"], style: { foreground: "gray" } },
|
|
17
|
+
{ scope: ["markup.link", "markup.link.url"], style: { foreground: "cyan", underline: true } },
|
|
18
|
+
{ scope: ["markup.link.label", "markup.link.bracket.close"], style: { foreground: "blue" } },
|
|
19
|
+
{ scope: ["markup.strikethrough"], style: { foreground: "gray", dim: true } },
|
|
20
|
+
{ scope: ["label"], style: { foreground: "gray", dim: true } },
|
|
21
|
+
{ scope: ["punctuation.special", "punctuation.delimiter"], style: { foreground: "gray" } },
|
|
22
|
+
{ scope: ["string.escape"], style: { foreground: "magenta" } },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const markdownSyntaxStyle = SyntaxStyle.fromTheme(MARKDOWN_SYNTAX_THEME);
|
|
26
|
+
|
|
27
|
+
export interface Message {
|
|
28
|
+
id: string;
|
|
29
|
+
role: "user" | "assistant" | "tool";
|
|
30
|
+
content: string;
|
|
31
|
+
timestamp: Date;
|
|
32
|
+
toolName?: string;
|
|
33
|
+
command?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MessageItemProps {
|
|
37
|
+
message: Message;
|
|
38
|
+
status: AppStatus;
|
|
39
|
+
isLastToolMessage: boolean;
|
|
40
|
+
isLastUserMessage?: boolean;
|
|
41
|
+
hasAssistantResponse?: boolean;
|
|
42
|
+
streaming?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* MarkdownText - Renders markdown content with syntax highlighting
|
|
47
|
+
*/
|
|
48
|
+
function MarkdownText(props: { content: string; streaming?: boolean }): JSX.Element {
|
|
49
|
+
return (
|
|
50
|
+
<code
|
|
51
|
+
filetype="markdown"
|
|
52
|
+
content={props.content.trim()}
|
|
53
|
+
syntaxStyle={markdownSyntaxStyle}
|
|
54
|
+
conceal={true}
|
|
55
|
+
drawUnstyledText={false}
|
|
56
|
+
streaming={props.streaming ?? false}
|
|
57
|
+
fg={colors.text.primary}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* MessageItem - Individual message rendering with role-based styling
|
|
64
|
+
* OpenCode-inspired layout:
|
|
65
|
+
* - User messages: left border with content
|
|
66
|
+
* - Tool calls: bordered box showing command
|
|
67
|
+
* - Assistant messages: plain markdown content
|
|
68
|
+
*/
|
|
69
|
+
export function MessageItem(props: MessageItemProps): JSX.Element {
|
|
70
|
+
// Note: Don't destructure reactive props in SolidJS - access via props.xxx to maintain reactivity
|
|
71
|
+
const { message, streaming } = props;
|
|
72
|
+
|
|
73
|
+
// Show investigating spinner below user message when agent is working but no assistant response yet
|
|
74
|
+
// Access props directly to maintain SolidJS reactivity
|
|
75
|
+
const showInvestigatingSpinner = () =>
|
|
76
|
+
props.isLastUserMessage && props.status === "investigating" && !props.hasAssistantResponse;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<box flexDirection="column">
|
|
80
|
+
{/* User message - left border, no label (opencode style) */}
|
|
81
|
+
<Show when={message.role === "user"}>
|
|
82
|
+
<box flexDirection="column">
|
|
83
|
+
{/* Message with left border using text character */}
|
|
84
|
+
<box flexDirection="row">
|
|
85
|
+
<text fg={colors.border.muted}>│ </text>
|
|
86
|
+
<text fg={colors.text.primary}>{message.content}</text>
|
|
87
|
+
</box>
|
|
88
|
+
{/* Spinner during investigation (shown below user message) */}
|
|
89
|
+
<Show when={showInvestigatingSpinner()}>
|
|
90
|
+
<box flexDirection="row" paddingLeft={2}>
|
|
91
|
+
<spinner name="dots" color={createPulse(["cyan", "blue", "magenta"], 200)} />
|
|
92
|
+
<text fg={colors.text.secondary} attributes={ATTR_ITALIC}> Investigating...</text>
|
|
93
|
+
</box>
|
|
94
|
+
</Show>
|
|
95
|
+
</box>
|
|
96
|
+
</Show>
|
|
97
|
+
|
|
98
|
+
{/* Tool message - bordered box showing command (opencode style) */}
|
|
99
|
+
<Show when={message.role === "tool"}>
|
|
100
|
+
<box
|
|
101
|
+
borderStyle="single"
|
|
102
|
+
borderColor={colors.role.tool}
|
|
103
|
+
paddingLeft={1}
|
|
104
|
+
paddingRight={1}
|
|
105
|
+
>
|
|
106
|
+
<box flexDirection="row" gap={1}>
|
|
107
|
+
{/* Status indicator - checkmark for completed, pause for awaiting approval */}
|
|
108
|
+
<Show when={props.status === "awaiting_approval" && props.isLastToolMessage} fallback={<text fg={colors.success}>✓</text>}>
|
|
109
|
+
<text fg={colors.warning}>⏸</text>
|
|
110
|
+
</Show>
|
|
111
|
+
{/* Tool name badge */}
|
|
112
|
+
<text fg={colors.role.tool} attributes={ATTR_BOLD}>
|
|
113
|
+
[{message.toolName}]
|
|
114
|
+
</text>
|
|
115
|
+
{/* Command */}
|
|
116
|
+
<text fg={colors.text.secondary}>
|
|
117
|
+
{message.content}
|
|
118
|
+
</text>
|
|
119
|
+
</box>
|
|
120
|
+
</box>
|
|
121
|
+
</Show>
|
|
122
|
+
|
|
123
|
+
{/* Assistant message - no label, direct markdown content (opencode style) */}
|
|
124
|
+
<Show when={message.role === "assistant"}>
|
|
125
|
+
<box flexDirection="column" paddingLeft={1}>
|
|
126
|
+
<MarkdownText content={message.content} streaming={streaming} />
|
|
127
|
+
</box>
|
|
128
|
+
</Show>
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { For, Show } from "solid-js";
|
|
3
|
+
import type { JSX } from "solid-js";
|
|
4
|
+
import { colors, spacing, ATTR_DIM, type AppStatus } from "../theme/index.js";
|
|
5
|
+
import { MessageItem, type Message } from "./message-item.js";
|
|
6
|
+
export type { Message };
|
|
7
|
+
|
|
8
|
+
export interface MessagesPanelProps {
|
|
9
|
+
messages: Message[];
|
|
10
|
+
status: AppStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MessagesPanel - Scrollable messages container with empty state
|
|
15
|
+
*/
|
|
16
|
+
export function MessagesPanel(props: MessagesPanelProps): JSX.Element {
|
|
17
|
+
const getLastToolMessageId = (): string | undefined => {
|
|
18
|
+
const toolMessages = props.messages.filter((m) => m.role === "tool");
|
|
19
|
+
return toolMessages.at(-1)?.id;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getLastUserMessageId = (): string | undefined => {
|
|
23
|
+
const userMessages = props.messages.filter((m) => m.role === "user");
|
|
24
|
+
return userMessages.at(-1)?.id;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const hasToolMessages = (): boolean => {
|
|
28
|
+
return props.messages.some((m) => m.role === "tool");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const hasAssistantMessages = (): boolean => {
|
|
32
|
+
return props.messages.some((m) => m.role === "assistant");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<scrollbox
|
|
37
|
+
flexGrow={1}
|
|
38
|
+
borderStyle="single"
|
|
39
|
+
borderColor={colors.border.default}
|
|
40
|
+
paddingLeft={spacing.xs}
|
|
41
|
+
paddingRight={spacing.xs}
|
|
42
|
+
stickyScroll
|
|
43
|
+
stickyStart="bottom"
|
|
44
|
+
>
|
|
45
|
+
<box flexDirection="column" gap={0}>
|
|
46
|
+
<Show
|
|
47
|
+
when={props.messages.length > 0}
|
|
48
|
+
fallback={
|
|
49
|
+
<box paddingTop={spacing.sm} paddingBottom={spacing.sm}>
|
|
50
|
+
<text fg={colors.text.secondary} attributes={ATTR_DIM}>
|
|
51
|
+
Enter an incident description to start investigating...
|
|
52
|
+
</text>
|
|
53
|
+
</box>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<For each={props.messages}>
|
|
57
|
+
{(msg) => (
|
|
58
|
+
<MessageItem
|
|
59
|
+
message={msg}
|
|
60
|
+
status={props.status}
|
|
61
|
+
isLastToolMessage={msg.id === getLastToolMessageId()}
|
|
62
|
+
isLastUserMessage={msg.id === getLastUserMessageId()}
|
|
63
|
+
hasAssistantResponse={hasAssistantMessages()}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
</For>
|
|
67
|
+
</Show>
|
|
68
|
+
</box>
|
|
69
|
+
</scrollbox>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
import { ATTR_BOLD, getStatusColor, getStatusText, type AppStatus } from "../theme/index.js";
|
|
4
|
+
|
|
5
|
+
export interface StatusBadgeProps {
|
|
6
|
+
status: AppStatus;
|
|
7
|
+
currentTool?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* StatusBadge - Status indicator showing Ready/Investigating/Awaiting Approval/Error
|
|
12
|
+
* Displays current app state with appropriate color coding
|
|
13
|
+
*/
|
|
14
|
+
export function StatusBadge(props: StatusBadgeProps): JSX.Element {
|
|
15
|
+
return (
|
|
16
|
+
<text fg={getStatusColor(props.status)} attributes={ATTR_BOLD}>
|
|
17
|
+
[{getStatusText(props.status, props.currentTool)}]
|
|
18
|
+
</text>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Show } from "solid-js";
|
|
3
|
+
import type { JSX } from "solid-js";
|
|
4
|
+
import { colors, spacing, ATTR_BOLD, ATTR_DIM, type AppStatus } from "../theme/index.js";
|
|
5
|
+
|
|
6
|
+
export interface StatusBarProps {
|
|
7
|
+
status: AppStatus;
|
|
8
|
+
messageCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* StatusBar - Bottom bar showing mode, keyboard hints, and message count
|
|
13
|
+
*/
|
|
14
|
+
export function StatusBar(props: StatusBarProps): JSX.Element {
|
|
15
|
+
return (
|
|
16
|
+
<box
|
|
17
|
+
paddingLeft={spacing.sm}
|
|
18
|
+
paddingRight={spacing.sm}
|
|
19
|
+
flexDirection="row"
|
|
20
|
+
justifyContent="space-between"
|
|
21
|
+
>
|
|
22
|
+
<Show
|
|
23
|
+
when={props.status === "awaiting_approval"}
|
|
24
|
+
fallback={
|
|
25
|
+
<text fg={colors.text.secondary} attributes={ATTR_DIM}>
|
|
26
|
+
Press Enter to submit | Ctrl+C to quit
|
|
27
|
+
</text>
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
<text fg={colors.warning} attributes={ATTR_BOLD}>
|
|
31
|
+
Approval required: Y/N or Enter
|
|
32
|
+
</text>
|
|
33
|
+
</Show>
|
|
34
|
+
<text fg={colors.text.secondary} attributes={ATTR_DIM}>
|
|
35
|
+
{props.messageCount} messages
|
|
36
|
+
</text>
|
|
37
|
+
</box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* Styled span component that properly types fg/bg/attributes props.
|
|
4
|
+
* This is a workaround for @opentui/solid's SpanProps not including TextNodeOptions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { JSX } from "solid-js";
|
|
8
|
+
import type { RGBA } from "@opentui/core";
|
|
9
|
+
|
|
10
|
+
export interface StyledSpanProps {
|
|
11
|
+
children?: JSX.Element | string | number | (JSX.Element | string | number)[];
|
|
12
|
+
fg?: string | RGBA;
|
|
13
|
+
bg?: string | RGBA;
|
|
14
|
+
attributes?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A span element with proper typing for fg, bg, and attributes props.
|
|
19
|
+
* Use this instead of <span> when you need color styling.
|
|
20
|
+
*/
|
|
21
|
+
export function StyledSpan(props: StyledSpanProps): JSX.Element {
|
|
22
|
+
// Cast to any to bypass type checking since the runtime supports these props
|
|
23
|
+
return <span {...(props as any)} />;
|
|
24
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { For, Show, type JSX } from "solid-js";
|
|
3
|
+
import { createTextAttributes } from "@opentui/core";
|
|
4
|
+
import type { InvestigationEvent, ToolCallRecord } from "../../storage/types.js";
|
|
5
|
+
|
|
6
|
+
const ATTR_DIM = createTextAttributes({ dim: true });
|
|
7
|
+
const ATTR_BOLD = createTextAttributes({ bold: true });
|
|
8
|
+
|
|
9
|
+
export interface TimelineEvent {
|
|
10
|
+
id: string;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
type: "tool_call" | "alert" | "k8s_event" | "log_entry" | "user_action";
|
|
13
|
+
title: string;
|
|
14
|
+
details?: string;
|
|
15
|
+
severity?: "critical" | "warning" | "info" | "success";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TimelineProps {
|
|
19
|
+
events: TimelineEvent[];
|
|
20
|
+
maxHeight?: number;
|
|
21
|
+
showTimestamps?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getSeverityColor(severity?: TimelineEvent["severity"]): string {
|
|
25
|
+
switch (severity) {
|
|
26
|
+
case "critical":
|
|
27
|
+
return "red";
|
|
28
|
+
case "warning":
|
|
29
|
+
return "yellow";
|
|
30
|
+
case "success":
|
|
31
|
+
return "green";
|
|
32
|
+
case "info":
|
|
33
|
+
default:
|
|
34
|
+
return "blue";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getEventIcon(type: TimelineEvent["type"]): string {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "tool_call":
|
|
41
|
+
return "⚙";
|
|
42
|
+
case "alert":
|
|
43
|
+
return "🔔";
|
|
44
|
+
case "k8s_event":
|
|
45
|
+
return "☸";
|
|
46
|
+
case "log_entry":
|
|
47
|
+
return "📝";
|
|
48
|
+
case "user_action":
|
|
49
|
+
return "👤";
|
|
50
|
+
default:
|
|
51
|
+
return "•";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatTime(date: Date): string {
|
|
56
|
+
return date.toLocaleTimeString("en-US", {
|
|
57
|
+
hour: "2-digit",
|
|
58
|
+
minute: "2-digit",
|
|
59
|
+
second: "2-digit",
|
|
60
|
+
hour12: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatRelativeTime(date: Date): string {
|
|
65
|
+
const now = new Date();
|
|
66
|
+
const diffMs = now.getTime() - date.getTime();
|
|
67
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
68
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
69
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
70
|
+
|
|
71
|
+
if (diffSec < 60) {
|
|
72
|
+
return `${diffSec}s ago`;
|
|
73
|
+
} else if (diffMin < 60) {
|
|
74
|
+
return `${diffMin}m ago`;
|
|
75
|
+
} else if (diffHour < 24) {
|
|
76
|
+
return `${diffHour}h ago`;
|
|
77
|
+
} else {
|
|
78
|
+
return date.toLocaleDateString();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function Timeline(props: TimelineProps): JSX.Element {
|
|
83
|
+
const showTimestamps = props.showTimestamps ?? true;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<box flexDirection="column" gap={0}>
|
|
87
|
+
<Show
|
|
88
|
+
when={props.events.length > 0}
|
|
89
|
+
fallback={
|
|
90
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
91
|
+
No events to display
|
|
92
|
+
</text>
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
<For each={props.events}>
|
|
96
|
+
{(event, index) => {
|
|
97
|
+
const color = getSeverityColor(event.severity);
|
|
98
|
+
const icon = getEventIcon(event.type);
|
|
99
|
+
const isLast = index() === props.events.length - 1;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<box flexDirection="row" gap={1}>
|
|
103
|
+
{/* Timeline line */}
|
|
104
|
+
<box flexDirection="column" width={3} alignItems="center">
|
|
105
|
+
<text fg={color}>{icon}</text>
|
|
106
|
+
<Show when={!isLast}>
|
|
107
|
+
<text fg="gray" attributes={ATTR_DIM}>│</text>
|
|
108
|
+
</Show>
|
|
109
|
+
</box>
|
|
110
|
+
|
|
111
|
+
{/* Event content */}
|
|
112
|
+
<box flexDirection="column" flexGrow={1}>
|
|
113
|
+
<box flexDirection="row" gap={1}>
|
|
114
|
+
<text fg={color} attributes={ATTR_BOLD}>
|
|
115
|
+
{event.title}
|
|
116
|
+
</text>
|
|
117
|
+
<Show when={showTimestamps}>
|
|
118
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
119
|
+
({formatRelativeTime(event.timestamp)})
|
|
120
|
+
</text>
|
|
121
|
+
</Show>
|
|
122
|
+
</box>
|
|
123
|
+
<Show when={event.details}>
|
|
124
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
125
|
+
{event.details}
|
|
126
|
+
</text>
|
|
127
|
+
</Show>
|
|
128
|
+
</box>
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}}
|
|
132
|
+
</For>
|
|
133
|
+
</Show>
|
|
134
|
+
</box>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Helper to convert investigation events to timeline events
|
|
139
|
+
export function investigationEventsToTimeline(
|
|
140
|
+
events: InvestigationEvent[],
|
|
141
|
+
toolCalls: ToolCallRecord[]
|
|
142
|
+
): TimelineEvent[] {
|
|
143
|
+
const timelineEvents: TimelineEvent[] = [];
|
|
144
|
+
|
|
145
|
+
// Add tool calls
|
|
146
|
+
for (const tc of toolCalls) {
|
|
147
|
+
timelineEvents.push({
|
|
148
|
+
id: tc.id,
|
|
149
|
+
timestamp: tc.timestamp,
|
|
150
|
+
type: "tool_call",
|
|
151
|
+
title: `Tool: ${tc.toolName}`,
|
|
152
|
+
details: tc.args?.command ? `$ ${tc.args.command}` : undefined,
|
|
153
|
+
severity: tc.error ? "warning" : "success",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Add investigation events
|
|
158
|
+
for (const event of events) {
|
|
159
|
+
let severity: TimelineEvent["severity"] = "info";
|
|
160
|
+
if (event.type === "alert") {
|
|
161
|
+
severity = "warning";
|
|
162
|
+
} else if (event.type === "k8s_event") {
|
|
163
|
+
const data = event.data as { type?: string };
|
|
164
|
+
severity = data.type === "Warning" ? "warning" : "info";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
timelineEvents.push({
|
|
168
|
+
id: event.id,
|
|
169
|
+
timestamp: event.timestamp,
|
|
170
|
+
type: event.type,
|
|
171
|
+
title: event.source,
|
|
172
|
+
details: JSON.stringify(event.data).slice(0, 100),
|
|
173
|
+
severity,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort by timestamp descending (most recent first)
|
|
178
|
+
timelineEvents.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
179
|
+
|
|
180
|
+
return timelineEvents;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface CompactTimelineProps {
|
|
184
|
+
events: TimelineEvent[];
|
|
185
|
+
maxEvents?: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function CompactTimeline(props: CompactTimelineProps): JSX.Element {
|
|
189
|
+
const maxEvents = props.maxEvents ?? 5;
|
|
190
|
+
const displayEvents = () => props.events.slice(0, maxEvents);
|
|
191
|
+
const hasMore = () => props.events.length > maxEvents;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<box flexDirection="column">
|
|
195
|
+
<text fg="cyan" attributes={ATTR_BOLD}>
|
|
196
|
+
Recent Events
|
|
197
|
+
</text>
|
|
198
|
+
<box flexDirection="column" marginTop={1}>
|
|
199
|
+
<For each={displayEvents()}>
|
|
200
|
+
{(event) => {
|
|
201
|
+
const color = getSeverityColor(event.severity);
|
|
202
|
+
const icon = getEventIcon(event.type);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<box flexDirection="row" gap={1}>
|
|
206
|
+
<text fg={color}>{icon}</text>
|
|
207
|
+
<text fg="white">{event.title}</text>
|
|
208
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
209
|
+
{formatRelativeTime(event.timestamp)}
|
|
210
|
+
</text>
|
|
211
|
+
</box>
|
|
212
|
+
);
|
|
213
|
+
}}
|
|
214
|
+
</For>
|
|
215
|
+
<Show when={hasMore()}>
|
|
216
|
+
<text fg="gray" attributes={ATTR_DIM}>
|
|
217
|
+
... and {props.events.length - maxEvents} more events
|
|
218
|
+
</text>
|
|
219
|
+
</Show>
|
|
220
|
+
</box>
|
|
221
|
+
</box>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Toaster, toast, useToasts } from "@opentui-ui/toast/solid";
|
|
3
|
+
import { TOAST_DURATION } from "@opentui-ui/toast";
|
|
4
|
+
import type { JSX } from "solid-js";
|
|
5
|
+
import { colors, spacing } from "../theme/index.js";
|
|
6
|
+
|
|
7
|
+
// Re-export toast function and duration constants for app-wide use
|
|
8
|
+
export { toast, TOAST_DURATION, useToasts };
|
|
9
|
+
|
|
10
|
+
export interface ToastProviderProps {
|
|
11
|
+
children: JSX.Element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Toast provider component that wraps the application
|
|
16
|
+
* Provides toast notifications positioned at the bottom-right
|
|
17
|
+
*/
|
|
18
|
+
export function ToastProvider(props: ToastProviderProps): JSX.Element {
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{props.children}
|
|
22
|
+
<Toaster
|
|
23
|
+
position="bottom-right"
|
|
24
|
+
gap={1}
|
|
25
|
+
maxWidth={50}
|
|
26
|
+
closeButton={true}
|
|
27
|
+
toastOptions={{
|
|
28
|
+
duration: TOAST_DURATION.DEFAULT,
|
|
29
|
+
style: {
|
|
30
|
+
borderStyle: "single",
|
|
31
|
+
borderColor: colors.border.default,
|
|
32
|
+
backgroundColor: colors.background.primary,
|
|
33
|
+
paddingLeft: spacing.xs,
|
|
34
|
+
paddingRight: spacing.xs,
|
|
35
|
+
paddingTop: 0,
|
|
36
|
+
paddingBottom: 0,
|
|
37
|
+
},
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Helper functions for common toast patterns
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Show a success toast notification
|
|
48
|
+
*/
|
|
49
|
+
export function toastSuccess(message: string, description?: string) {
|
|
50
|
+
return toast.success(message, { description, duration: TOAST_DURATION.SHORT });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Show an error toast notification
|
|
55
|
+
*/
|
|
56
|
+
export function toastError(message: string, description?: string) {
|
|
57
|
+
return toast.error(message, { description, duration: TOAST_DURATION.LONG });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Show a warning toast notification
|
|
62
|
+
*/
|
|
63
|
+
export function toastWarning(message: string, description?: string) {
|
|
64
|
+
return toast.warning(message, { description, duration: TOAST_DURATION.LONG });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Show an info toast notification
|
|
69
|
+
*/
|
|
70
|
+
export function toastInfo(message: string, description?: string) {
|
|
71
|
+
return toast.info(message, { description, duration: TOAST_DURATION.DEFAULT });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Show a loading toast that can be updated when operation completes
|
|
76
|
+
*/
|
|
77
|
+
export function toastLoading(message: string) {
|
|
78
|
+
return toast.loading(message, { duration: TOAST_DURATION.PERSISTENT });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Dismiss a specific toast or all toasts
|
|
83
|
+
*/
|
|
84
|
+
export function toastDismiss(id?: string | number) {
|
|
85
|
+
if (id !== undefined) {
|
|
86
|
+
toast.dismiss(id);
|
|
87
|
+
} else {
|
|
88
|
+
toast.dismiss();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Show toast with promise handling - loading, success, error states
|
|
94
|
+
*/
|
|
95
|
+
export function toastPromise<T>(
|
|
96
|
+
promise: Promise<T>,
|
|
97
|
+
messages: {
|
|
98
|
+
loading?: string;
|
|
99
|
+
success?: string | ((data: T) => string);
|
|
100
|
+
error?: string | ((err: unknown) => string);
|
|
101
|
+
}
|
|
102
|
+
) {
|
|
103
|
+
return toast.promise(promise, messages);
|
|
104
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme exports for Triagent TUI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
colors,
|
|
7
|
+
spacing,
|
|
8
|
+
layout,
|
|
9
|
+
ATTR_BOLD,
|
|
10
|
+
ATTR_DIM,
|
|
11
|
+
ATTR_ITALIC,
|
|
12
|
+
ATTR_UNDERLINE,
|
|
13
|
+
ATTR_BOLD_DIM,
|
|
14
|
+
getRiskColor,
|
|
15
|
+
getRiskHexColor,
|
|
16
|
+
getRiskEmoji,
|
|
17
|
+
getStatusColor,
|
|
18
|
+
getStatusText,
|
|
19
|
+
type RiskLevel,
|
|
20
|
+
type AppStatus,
|
|
21
|
+
} from "./tokens.js";
|