@maidang1/hataraku 0.0.3
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/.claude/CLAUDE.md +21 -0
- package/.codex/skills/beautiful-mermaid/SKILL.md +171 -0
- package/.codex/skills/beautiful-mermaid/references/mermaid-syntax.md +235 -0
- package/.codex/skills/beautiful-mermaid/scripts/create-html.ts +177 -0
- package/.codex/skills/beautiful-mermaid/scripts/render.ts +221 -0
- package/.codex/skills/find-skills/SKILL.md +133 -0
- package/.github/workflows/publish-github-packages.yml +58 -0
- package/.github/workflows/publish-npm.yml +46 -0
- package/.vscode/settings.json +2 -0
- package/AGENTS.md +41 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/bun.lock +327 -0
- package/docs/agent/architecture.md +28 -0
- package/docs/agent/development_commands.md +6 -0
- package/docs/plan/agent-plan-2026-02-05.md +136 -0
- package/docs/plan/core-agent-sdk-structure-2026-02-07.md +156 -0
- package/docs/plan/implementation-summary.md +303 -0
- package/docs/plan/mcp-2026-02-05.md +700 -0
- package/docs/plan/op.md +478 -0
- package/docs/plan/skills-2026-02-05.md +352 -0
- package/docs/plan/skills-flow.svg +120 -0
- package/docs/plan/tui-readability-2026-02-06.md +67 -0
- package/package.json +34 -0
- package/src/cli/index.tsx +4 -0
- package/src/cli/main.tsx +98 -0
- package/src/core/README.md +19 -0
- package/src/core/api/agent.ts +1 -0
- package/src/core/api/config.ts +1 -0
- package/src/core/api/index.ts +10 -0
- package/src/core/api/integrations.ts +1 -0
- package/src/core/api/observability.ts +1 -0
- package/src/core/api/policy.ts +1 -0
- package/src/core/api/providers.ts +1 -0
- package/src/core/api/runtime.ts +1 -0
- package/src/core/api/shared.ts +1 -0
- package/src/core/api/tools.ts +1 -0
- package/src/core/api/types.ts +1 -0
- package/src/core/index.ts +1 -0
- package/src/core/internal/config/defaults.ts +8 -0
- package/src/core/internal/config/index.ts +3 -0
- package/src/core/internal/config/loader.ts +97 -0
- package/src/core/internal/config/schema.ts +47 -0
- package/src/core/internal/integrations/index.ts +2 -0
- package/src/core/internal/integrations/mcp/connection-manager.ts +231 -0
- package/src/core/internal/integrations/mcp/health-checker.ts +91 -0
- package/src/core/internal/integrations/mcp/index.ts +197 -0
- package/src/core/internal/integrations/mcp/retry-strategy.ts +111 -0
- package/src/core/internal/integrations/mcp/tool-cache.ts +103 -0
- package/src/core/internal/integrations/mcp/transport.ts +58 -0
- package/src/core/internal/integrations/mcp/types.ts +95 -0
- package/src/core/internal/integrations/mcp/utils.ts +44 -0
- package/src/core/internal/integrations/skills/cache/index.ts +38 -0
- package/src/core/internal/integrations/skills/cache/interface.ts +9 -0
- package/src/core/internal/integrations/skills/cache/memory-cache.ts +118 -0
- package/src/core/internal/integrations/skills/config/defaults.ts +35 -0
- package/src/core/internal/integrations/skills/config/index.ts +71 -0
- package/src/core/internal/integrations/skills/config/schema.ts +31 -0
- package/src/core/internal/integrations/skills/core/errors.ts +36 -0
- package/src/core/internal/integrations/skills/core/events.ts +143 -0
- package/src/core/internal/integrations/skills/core/types.ts +83 -0
- package/src/core/internal/integrations/skills/dependency/conflict-detector.ts +126 -0
- package/src/core/internal/integrations/skills/dependency/graph.ts +91 -0
- package/src/core/internal/integrations/skills/dependency/resolver.ts +128 -0
- package/src/core/internal/integrations/skills/dependency/types.ts +51 -0
- package/src/core/internal/integrations/skills/discovery/index.ts +98 -0
- package/src/core/internal/integrations/skills/discovery/resolver.ts +39 -0
- package/src/core/internal/integrations/skills/discovery/scanner.ts +116 -0
- package/src/core/internal/integrations/skills/discovery/strategies/file-system.ts +16 -0
- package/src/core/internal/integrations/skills/index.ts +3 -0
- package/src/core/internal/integrations/skills/integration/lifecycle.ts +124 -0
- package/src/core/internal/integrations/skills/integration/mcp-loader.ts +100 -0
- package/src/core/internal/integrations/skills/integration/tool-mapper.ts +56 -0
- package/src/core/internal/integrations/skills/loaders/index.ts +5 -0
- package/src/core/internal/integrations/skills/loaders/skill-loader.ts +97 -0
- package/src/core/internal/integrations/skills/manager.ts +200 -0
- package/src/core/internal/integrations/skills/parsers/base.ts +134 -0
- package/src/core/internal/integrations/skills/parsers/factory.ts +42 -0
- package/src/core/internal/integrations/skills/parsers/index.ts +71 -0
- package/src/core/internal/integrations/skills/parsers/markdown.ts +111 -0
- package/src/core/internal/integrations/skills/parsers/yaml-metadata.ts +49 -0
- package/src/core/internal/integrations/skills/types.ts +15 -0
- package/src/core/internal/integrations/skills/utils/fs.ts +59 -0
- package/src/core/internal/integrations/skills/utils/logger.ts +109 -0
- package/src/core/internal/integrations/skills/utils/path.ts +27 -0
- package/src/core/internal/integrations/skills/validation/index.ts +43 -0
- package/src/core/internal/integrations/skills/validation/schema.ts +37 -0
- package/src/core/internal/integrations/skills/validation/skill-validator.ts +56 -0
- package/src/core/internal/observability/index.ts +2 -0
- package/src/core/internal/observability/logging/env.ts +32 -0
- package/src/core/internal/observability/logging/export.ts +55 -0
- package/src/core/internal/observability/logging/index.ts +4 -0
- package/src/core/internal/observability/logging/session-logger.ts +54 -0
- package/src/core/internal/observability/logging/types.ts +53 -0
- package/src/core/internal/policy/index.ts +1 -0
- package/src/core/internal/policy/safety/index.ts +2 -0
- package/src/core/internal/policy/safety/policy.ts +96 -0
- package/src/core/internal/policy/safety/types.ts +24 -0
- package/src/core/internal/providers/anthropic/client.ts +20 -0
- package/src/core/internal/providers/anthropic/index.ts +1 -0
- package/src/core/internal/providers/index.ts +1 -0
- package/src/core/internal/sdk/agent/agent.ts +691 -0
- package/src/core/internal/sdk/agent/index.ts +3 -0
- package/src/core/internal/sdk/agent/session.ts +9 -0
- package/src/core/internal/sdk/agent/tool-loop.ts +10 -0
- package/src/core/internal/sdk/index.ts +3 -0
- package/src/core/internal/sdk/runtime/context.ts +1 -0
- package/src/core/internal/sdk/runtime/errors.ts +9 -0
- package/src/core/internal/sdk/runtime/execution.ts +12 -0
- package/src/core/internal/sdk/runtime/index.ts +3 -0
- package/src/core/internal/sdk/types/api.ts +4 -0
- package/src/core/internal/sdk/types/index.ts +1 -0
- package/src/core/internal/sdk/types/internal.ts +1 -0
- package/src/core/internal/shared/fs.ts +10 -0
- package/src/core/internal/shared/index.ts +3 -0
- package/src/core/internal/shared/message.ts +12 -0
- package/src/core/internal/shared/path.ts +10 -0
- package/src/core/internal/tools/base/errors.ts +6 -0
- package/src/core/internal/tools/base/index.ts +3 -0
- package/src/core/internal/tools/base/schema.ts +1 -0
- package/src/core/internal/tools/base/tool.ts +42 -0
- package/src/core/internal/tools/builtins/architect.ts +45 -0
- package/src/core/internal/tools/builtins/bash.ts +135 -0
- package/src/core/internal/tools/builtins/fetch.ts +62 -0
- package/src/core/internal/tools/builtins/file-edit.ts +134 -0
- package/src/core/internal/tools/builtins/file-read.ts +75 -0
- package/src/core/internal/tools/builtins/fs.ts +254 -0
- package/src/core/internal/tools/builtins/glob.ts +75 -0
- package/src/core/internal/tools/builtins/grep.ts +104 -0
- package/src/core/internal/tools/builtins/index.ts +26 -0
- package/src/core/internal/tools/builtins/list-files.ts +64 -0
- package/src/core/internal/tools/builtins/search.ts +50 -0
- package/src/core/internal/tools/builtins/skills.ts +127 -0
- package/src/core/internal/tools/builtins/todo.ts +121 -0
- package/src/core/internal/tools/guards/file-edit-cache.ts +21 -0
- package/src/core/internal/tools/guards/limits.ts +43 -0
- package/src/core/internal/tools/index.ts +39 -0
- package/src/core/internal/tools/registry/index.ts +2 -0
- package/src/core/internal/tools/registry/presets.ts +28 -0
- package/src/core/internal/tools/registry/registry.ts +21 -0
- package/src/index.ts +3 -0
- package/src/render/commands/index.ts +113 -0
- package/src/render/commands/init.ts +45 -0
- package/src/render/components/ActivityPane.tsx +67 -0
- package/src/render/components/ChatBubble.tsx +58 -0
- package/src/render/components/ConfirmCard.tsx +100 -0
- package/src/render/components/ConfirmSelectMenu.tsx +56 -0
- package/src/render/components/ConversationPane.tsx +65 -0
- package/src/render/components/EventTimeline.tsx +30 -0
- package/src/render/components/MarkdownText.tsx +139 -0
- package/src/render/components/SlashCommandMenu.tsx +68 -0
- package/src/render/components/Spinner.tsx +18 -0
- package/src/render/components/StatusBar.tsx +72 -0
- package/src/render/components/Timeline.tsx +57 -0
- package/src/render/components/TimelineEvent.tsx +313 -0
- package/src/render/components/ToolCard.tsx +126 -0
- package/src/render/components/formatters/confirm.test.ts +34 -0
- package/src/render/components/formatters/confirm.ts +32 -0
- package/src/render/index.tsx +466 -0
- package/src/render/state/events.ts +301 -0
- package/src/render/state/history.ts +5 -0
- package/src/render/state/loading.ts +18 -0
- package/src/render/state/message.tsx +35 -0
- package/src/render/state/store.ts +7 -0
- package/src/render/theme.ts +52 -0
- package/test-e2e.ts +250 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { MarkdownText } from "./MarkdownText";
|
|
4
|
+
import { Spinner } from "./Spinner";
|
|
5
|
+
import {
|
|
6
|
+
formatConfirmPreview,
|
|
7
|
+
formatConfirmReason,
|
|
8
|
+
shouldShowConfirmReason,
|
|
9
|
+
} from "./formatters/confirm";
|
|
10
|
+
import type {
|
|
11
|
+
ChatEvent,
|
|
12
|
+
ConfirmEvent,
|
|
13
|
+
ErrorEvent,
|
|
14
|
+
McpEvent,
|
|
15
|
+
ThinkingEvent,
|
|
16
|
+
ToolEvent,
|
|
17
|
+
UiEvent,
|
|
18
|
+
} from "../state/events";
|
|
19
|
+
import { COLORS, INDENT, SPACE, TEXT } from "../theme";
|
|
20
|
+
|
|
21
|
+
const TOOL_RESULT_PREVIEW_MAX = 400;
|
|
22
|
+
const TOOL_META_PREVIEW_MAX = 56;
|
|
23
|
+
|
|
24
|
+
type EventHeaderProps = {
|
|
25
|
+
icon: string;
|
|
26
|
+
iconColor: string;
|
|
27
|
+
title: string;
|
|
28
|
+
titleColor?: string;
|
|
29
|
+
meta?: string;
|
|
30
|
+
trailing?: React.ReactNode;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function EventHeader({
|
|
34
|
+
icon,
|
|
35
|
+
iconColor,
|
|
36
|
+
title,
|
|
37
|
+
titleColor = TEXT.primary,
|
|
38
|
+
meta,
|
|
39
|
+
trailing,
|
|
40
|
+
}: EventHeaderProps): React.JSX.Element {
|
|
41
|
+
return (
|
|
42
|
+
<Box>
|
|
43
|
+
<Text color={iconColor}>{icon} </Text>
|
|
44
|
+
<Text color={titleColor} bold>
|
|
45
|
+
{title}
|
|
46
|
+
</Text>
|
|
47
|
+
{meta && <Text color={TEXT.dim}> {meta}</Text>}
|
|
48
|
+
{trailing ? <Text color={TEXT.dim}> {trailing}</Text> : null}
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function EventBody(props: { children: React.ReactNode }): React.JSX.Element {
|
|
54
|
+
return (
|
|
55
|
+
<Box flexDirection="column" paddingLeft={INDENT.sm} marginTop={SPACE.xs}>
|
|
56
|
+
{props.children}
|
|
57
|
+
</Box>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function EventHint(props: { text: string }): React.JSX.Element {
|
|
62
|
+
return (
|
|
63
|
+
<Box paddingLeft={INDENT.sm}>
|
|
64
|
+
<Text color={TEXT.dim}>{props.text}</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function oneLine(input: string, limit: number): string {
|
|
70
|
+
const normalized = input.replace(/\s*\n\s*/g, " ").trim();
|
|
71
|
+
if (normalized.length <= limit) return normalized;
|
|
72
|
+
return normalized.slice(0, Math.max(1, limit - 1)) + "…";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function durationMeta(event: ToolEvent): string | undefined {
|
|
76
|
+
if (!event.startedAt || !event.endedAt) return undefined;
|
|
77
|
+
const sec = Math.max(
|
|
78
|
+
1,
|
|
79
|
+
Math.round((Date.parse(event.endedAt) - Date.parse(event.startedAt)) / 1000),
|
|
80
|
+
);
|
|
81
|
+
return `(${sec}s)`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ChatEventRow(props: { event: ChatEvent }): React.JSX.Element {
|
|
85
|
+
const { event } = props;
|
|
86
|
+
|
|
87
|
+
if (event.role === "user") {
|
|
88
|
+
return (
|
|
89
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
90
|
+
<Box>
|
|
91
|
+
<Text color={COLORS.info}>❯ </Text>
|
|
92
|
+
{(event.content ?? "").split("\n").map((line, index) => (
|
|
93
|
+
<Text key={`${event.id}-user-${index}`} color={TEXT.primary}>
|
|
94
|
+
{line || " "}
|
|
95
|
+
</Text>
|
|
96
|
+
))}
|
|
97
|
+
</Box>
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (event.role === "system") {
|
|
103
|
+
return (
|
|
104
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
105
|
+
<Box>
|
|
106
|
+
<Text color={COLORS.warning}>◆ </Text>
|
|
107
|
+
{(event.content ?? "").split("\n").map((line, index) => (
|
|
108
|
+
<Text key={`${event.id}-sys-${index}`} color={TEXT.secondary}>
|
|
109
|
+
{line || " "}
|
|
110
|
+
</Text>
|
|
111
|
+
))}
|
|
112
|
+
</Box>
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
119
|
+
<Box>
|
|
120
|
+
<Text color={COLORS.accent}>● </Text>
|
|
121
|
+
<MarkdownText content={event.content ?? ""} />
|
|
122
|
+
</Box>
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ToolEventRow(props: { event: ToolEvent }): React.JSX.Element {
|
|
128
|
+
const { event } = props;
|
|
129
|
+
const isDone = event.status === "done";
|
|
130
|
+
const statusIcon = isDone ? "●" : "●";
|
|
131
|
+
const statusColor = isDone ? COLORS.accent : COLORS.pending;
|
|
132
|
+
const preview = oneLine(event.preview ?? `Run ${event.toolName}`, TOOL_META_PREVIEW_MAX);
|
|
133
|
+
|
|
134
|
+
const resultPreview = event.result
|
|
135
|
+
? oneLine(event.result, TOOL_RESULT_PREVIEW_MAX)
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
140
|
+
{/* Tool header with tree-style prefix */}
|
|
141
|
+
<Box>
|
|
142
|
+
<Text color={statusColor}>{statusIcon} </Text>
|
|
143
|
+
<Text color={COLORS.accent} bold>{event.toolName}</Text>
|
|
144
|
+
{isDone && <Text color={TEXT.dim}> {durationMeta(event)}</Text>}
|
|
145
|
+
{!isDone && (
|
|
146
|
+
<>
|
|
147
|
+
<Text color={TEXT.dim}> </Text>
|
|
148
|
+
<Spinner />
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
</Box>
|
|
152
|
+
|
|
153
|
+
{/* Tree-style nested content */}
|
|
154
|
+
{!event.expanded && (
|
|
155
|
+
<Box paddingLeft={INDENT.sm}>
|
|
156
|
+
<Text color={TEXT.dim}>└ </Text>
|
|
157
|
+
<Text color={TEXT.muted}>{preview}</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{event.expanded && (
|
|
162
|
+
<Box flexDirection="column" paddingLeft={INDENT.sm}>
|
|
163
|
+
{event.input !== undefined && (
|
|
164
|
+
<Box>
|
|
165
|
+
<Text color={TEXT.dim}>├ </Text>
|
|
166
|
+
<Text color={TEXT.muted}>
|
|
167
|
+
{typeof event.input === "string" ? event.input : JSON.stringify(event.input, null, 2)}
|
|
168
|
+
</Text>
|
|
169
|
+
</Box>
|
|
170
|
+
)}
|
|
171
|
+
{resultPreview && (
|
|
172
|
+
<Box>
|
|
173
|
+
<Text color={TEXT.dim}>├ </Text>
|
|
174
|
+
<Text color={TEXT.dim}>{resultPreview}</Text>
|
|
175
|
+
</Box>
|
|
176
|
+
)}
|
|
177
|
+
{!!event.filesChanged?.length &&
|
|
178
|
+
event.filesChanged.map((filePath, idx) => (
|
|
179
|
+
<Box key={`${event.id}-${filePath}`}>
|
|
180
|
+
<Text color={TEXT.dim}>{idx === event.filesChanged!.length - 1 ? "└ " : "├ "}</Text>
|
|
181
|
+
<Text color={COLORS.success}>✎ {filePath}</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
))}
|
|
184
|
+
</Box>
|
|
185
|
+
)}
|
|
186
|
+
</Box>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ThinkingEventRow(props: { event: ThinkingEvent }): React.JSX.Element {
|
|
191
|
+
const { event } = props;
|
|
192
|
+
const content = event.redacted ? "[thinking is redacted]" : event.content ?? "";
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
196
|
+
<EventHeader icon="…" iconColor={TEXT.dim} title="thinking" titleColor={TEXT.dim} />
|
|
197
|
+
<EventBody>
|
|
198
|
+
{(content || "").split("\n").map((line, index) => (
|
|
199
|
+
<Text key={`${event.id}-thinking-${index}`} color={TEXT.dim}>
|
|
200
|
+
{line || " "}
|
|
201
|
+
</Text>
|
|
202
|
+
))}
|
|
203
|
+
</EventBody>
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ConfirmEventRow(props: {
|
|
209
|
+
event: ConfirmEvent;
|
|
210
|
+
isActive: boolean;
|
|
211
|
+
}): React.JSX.Element | null {
|
|
212
|
+
const { event, isActive } = props;
|
|
213
|
+
if (!event.resolved && isActive) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const preview = formatConfirmPreview(event.preview);
|
|
218
|
+
const reason = formatConfirmReason(event.reason);
|
|
219
|
+
const showReason = shouldShowConfirmReason(event.reason, event.preview);
|
|
220
|
+
|
|
221
|
+
if (event.resolved) {
|
|
222
|
+
const allowed = !!event.allowed;
|
|
223
|
+
return (
|
|
224
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
225
|
+
<EventHeader
|
|
226
|
+
icon={allowed ? "✓" : "✗"}
|
|
227
|
+
iconColor={allowed ? COLORS.success : COLORS.danger}
|
|
228
|
+
title={`Confirm ${allowed ? "allowed" : "denied"}`}
|
|
229
|
+
titleColor={TEXT.secondary}
|
|
230
|
+
meta={event.toolName}
|
|
231
|
+
/>
|
|
232
|
+
</Box>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
238
|
+
<EventHeader
|
|
239
|
+
icon="⚠"
|
|
240
|
+
iconColor={COLORS.warning}
|
|
241
|
+
title={`Confirm ${event.toolName}`}
|
|
242
|
+
titleColor={TEXT.primary}
|
|
243
|
+
/>
|
|
244
|
+
<EventBody>
|
|
245
|
+
{showReason && <Text color={TEXT.muted}>{reason}</Text>}
|
|
246
|
+
{preview && <Text color={TEXT.dim}>{preview}</Text>}
|
|
247
|
+
</EventBody>
|
|
248
|
+
{isActive && (
|
|
249
|
+
<EventHint text="↑/↓ select • Enter confirm • Esc deny" />
|
|
250
|
+
)}
|
|
251
|
+
</Box>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function McpEventRow(props: { event: McpEvent }): React.JSX.Element {
|
|
256
|
+
const { event } = props;
|
|
257
|
+
const color =
|
|
258
|
+
event.level === "error"
|
|
259
|
+
? COLORS.danger
|
|
260
|
+
: event.level === "warn"
|
|
261
|
+
? COLORS.warning
|
|
262
|
+
: TEXT.dim;
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
266
|
+
<EventHeader icon="🔌" iconColor={color} title={event.message} titleColor={color} />
|
|
267
|
+
</Box>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function ErrorEventRow(props: { event: ErrorEvent }): React.JSX.Element {
|
|
272
|
+
const { event } = props;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<Box flexDirection="column" marginBottom={SPACE.sm}>
|
|
276
|
+
<EventHeader icon="✗" iconColor={COLORS.danger} title="Error" titleColor={COLORS.danger} meta={oneLine(event.message, 100)} />
|
|
277
|
+
{event.expanded && event.stack && (
|
|
278
|
+
<EventBody>
|
|
279
|
+
<Text color={COLORS.errorMuted}>{event.stack}</Text>
|
|
280
|
+
</EventBody>
|
|
281
|
+
)}
|
|
282
|
+
</Box>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function TimelineEvent(props: {
|
|
287
|
+
event: UiEvent;
|
|
288
|
+
activeConfirmId: string | null;
|
|
289
|
+
}): React.JSX.Element | null {
|
|
290
|
+
const { event, activeConfirmId } = props;
|
|
291
|
+
|
|
292
|
+
switch (event.type) {
|
|
293
|
+
case "chat":
|
|
294
|
+
return <ChatEventRow event={event} />;
|
|
295
|
+
case "thinking":
|
|
296
|
+
return <ThinkingEventRow event={event} />;
|
|
297
|
+
case "tool":
|
|
298
|
+
return <ToolEventRow event={event} />;
|
|
299
|
+
case "confirm":
|
|
300
|
+
return (
|
|
301
|
+
<ConfirmEventRow
|
|
302
|
+
event={event}
|
|
303
|
+
isActive={event.confirmId === activeConfirmId}
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
case "mcp":
|
|
307
|
+
return <McpEventRow event={event} />;
|
|
308
|
+
case "error":
|
|
309
|
+
return <ErrorEventRow event={event} />;
|
|
310
|
+
default:
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { ToolEvent } from "../state/events";
|
|
4
|
+
import { COLORS } from "../theme";
|
|
5
|
+
|
|
6
|
+
function stringify(input: unknown): string {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.stringify(input, null, 2);
|
|
9
|
+
} catch {
|
|
10
|
+
return String(input);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function truncateLines(value: string, maxLines: number): { text: string; truncated: boolean } {
|
|
15
|
+
const lines = value.split("\n");
|
|
16
|
+
if (lines.length <= maxLines) return { text: value, truncated: false };
|
|
17
|
+
return { text: lines.slice(0, maxLines).join("\n") + "\n…", truncated: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Simple JSON syntax highlighting
|
|
21
|
+
function highlightJson(json: string): React.ReactNode {
|
|
22
|
+
return json.split(/(".*?"|\{|\}|\[|\]|:|,|\n)/).map((part, i) => {
|
|
23
|
+
if (part.match(/^".*?"$/)) {
|
|
24
|
+
return <Text key={i} color={COLORS.success}>{part}</Text>;
|
|
25
|
+
}
|
|
26
|
+
if (part.match(/^[{}\[\]]$/)) {
|
|
27
|
+
return <Text key={i} color={COLORS.accent}>{part}</Text>;
|
|
28
|
+
}
|
|
29
|
+
if (part === ':') {
|
|
30
|
+
return <Text key={i} color={COLORS.text}>{part}</Text>;
|
|
31
|
+
}
|
|
32
|
+
return <Text key={i} color={COLORS.muted}>{part}</Text>;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ToolCard(props: {
|
|
37
|
+
event: ToolEvent;
|
|
38
|
+
selected: boolean;
|
|
39
|
+
focused: boolean;
|
|
40
|
+
}): React.JSX.Element {
|
|
41
|
+
const { event, selected, focused } = props;
|
|
42
|
+
const isSelected = selected && focused;
|
|
43
|
+
const statusLabel = event.status === "done" ? "done" : "pending";
|
|
44
|
+
const statusColor = event.status === "done" ? COLORS.success : COLORS.pending;
|
|
45
|
+
const bgColor = isSelected ? COLORS.bgSelected : event.status === "done" ? COLORS.bgToolDone : COLORS.bgToolRunning;
|
|
46
|
+
const summary = event.summary ?? event.preview ?? (event.input ? truncateLines(stringify(event.input), 3).text : "(no input)");
|
|
47
|
+
const selectedPrefix = isSelected ? "▶ " : "";
|
|
48
|
+
const duration =
|
|
49
|
+
event.startedAt && event.endedAt
|
|
50
|
+
? `${Math.max(0, Date.parse(event.endedAt) - Date.parse(event.startedAt))}ms`
|
|
51
|
+
: event.status === "pending"
|
|
52
|
+
? "running"
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
if (!event.expanded) {
|
|
56
|
+
return (
|
|
57
|
+
<Box backgroundColor={bgColor}>
|
|
58
|
+
<Text dimColor color={isSelected ? COLORS.focus : COLORS.dim}>
|
|
59
|
+
{selectedPrefix}🔧 {event.toolName}
|
|
60
|
+
</Text>
|
|
61
|
+
<Text color={statusColor}> [{statusLabel}]</Text>
|
|
62
|
+
{duration && <Text color={COLORS.muted}> ({duration})</Text>}
|
|
63
|
+
<Text color={COLORS.muted}> — {summary}</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const borderColor = isSelected ? COLORS.focus : COLORS.border;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Box flexDirection="column" borderStyle="single" borderColor={borderColor} paddingLeft={1} paddingRight={1} backgroundColor={bgColor}>
|
|
72
|
+
<Box justifyContent="space-between">
|
|
73
|
+
<Text bold color={COLORS.text}>
|
|
74
|
+
{selectedPrefix}🔧 {event.toolName}
|
|
75
|
+
</Text>
|
|
76
|
+
<Text color={statusColor}>
|
|
77
|
+
{statusLabel}
|
|
78
|
+
{duration ? ` • ${duration}` : ""}
|
|
79
|
+
</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
|
|
82
|
+
{summary && (
|
|
83
|
+
<Box>
|
|
84
|
+
<Text color={COLORS.muted}>{summary}</Text>
|
|
85
|
+
</Box>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{event.input !== undefined && (
|
|
89
|
+
<Box flexDirection="column" paddingTop={1} paddingBottom={1}>
|
|
90
|
+
<Text bold color={COLORS.accent}>Input</Text>
|
|
91
|
+
<Box paddingLeft={2} paddingY={1}>
|
|
92
|
+
{highlightJson(stringify(event.input))}
|
|
93
|
+
</Box>
|
|
94
|
+
</Box>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{event.result !== undefined && (
|
|
98
|
+
<Box flexDirection="column" paddingTop={1} paddingBottom={1}>
|
|
99
|
+
<Text bold color={COLORS.accent}>Result</Text>
|
|
100
|
+
<Box paddingLeft={2} paddingY={1}>
|
|
101
|
+
<Text dimColor color={COLORS.dim}>{event.result || "(empty)"}</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
</Box>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{!!event.filesChanged?.length && (
|
|
107
|
+
<Box flexDirection="column" paddingTop={1} paddingBottom={1}>
|
|
108
|
+
<Text bold color={COLORS.accent}>Files Changed</Text>
|
|
109
|
+
<Box paddingLeft={1}>
|
|
110
|
+
{event.filesChanged.map((f) => (
|
|
111
|
+
<Text key={f} dimColor color={COLORS.dim}>
|
|
112
|
+
• {f}
|
|
113
|
+
</Text>
|
|
114
|
+
))}
|
|
115
|
+
</Box>
|
|
116
|
+
</Box>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<Box paddingTop={1}>
|
|
120
|
+
<Text dimColor color={COLORS.muted}>
|
|
121
|
+
Enter/e: toggle details • i: back to input
|
|
122
|
+
</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
</Box>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
formatConfirmPreview,
|
|
5
|
+
formatConfirmReason,
|
|
6
|
+
shouldShowConfirmReason,
|
|
7
|
+
} from "./confirm";
|
|
8
|
+
|
|
9
|
+
describe("confirm formatter", () => {
|
|
10
|
+
it("normalizes generic reason", () => {
|
|
11
|
+
expect(formatConfirmReason("Command execution requires confirmation")).toBe(
|
|
12
|
+
"Needs approval",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("normalizes command preview", () => {
|
|
17
|
+
expect(formatConfirmPreview("Run command:\n npm run test\n")).toBe(
|
|
18
|
+
"cmd: npm run test",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("hides generic reason when command preview exists", () => {
|
|
23
|
+
expect(
|
|
24
|
+
shouldShowConfirmReason(
|
|
25
|
+
"Command execution requires confirmation",
|
|
26
|
+
"Run command: bun test",
|
|
27
|
+
),
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("shows non-generic reason", () => {
|
|
32
|
+
expect(shouldShowConfirmReason("Path write required", undefined)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const GENERIC_COMMAND_CONFIRM_REASON = "Command execution requires confirmation";
|
|
2
|
+
|
|
3
|
+
export function formatConfirmReason(reason: string): string {
|
|
4
|
+
if (reason === GENERIC_COMMAND_CONFIRM_REASON) {
|
|
5
|
+
return "Needs approval";
|
|
6
|
+
}
|
|
7
|
+
return reason;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatConfirmPreview(preview?: string): string | undefined {
|
|
11
|
+
if (!preview) return undefined;
|
|
12
|
+
const normalized = preview.trim();
|
|
13
|
+
if (!normalized) return undefined;
|
|
14
|
+
|
|
15
|
+
if (normalized.startsWith("Run command:")) {
|
|
16
|
+
const command = normalized
|
|
17
|
+
.slice("Run command:".length)
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/\s*\n\s*/g, " ");
|
|
20
|
+
return command ? `cmd: ${command}` : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return normalized.replace(/\s*\n\s*/g, " ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shouldShowConfirmReason(
|
|
27
|
+
reason: string,
|
|
28
|
+
preview?: string,
|
|
29
|
+
): boolean {
|
|
30
|
+
const normalizedPreview = formatConfirmPreview(preview);
|
|
31
|
+
return !(reason === GENERIC_COMMAND_CONFIRM_REASON && normalizedPreview);
|
|
32
|
+
}
|