@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.
Files changed (167) hide show
  1. package/.claude/CLAUDE.md +21 -0
  2. package/.codex/skills/beautiful-mermaid/SKILL.md +171 -0
  3. package/.codex/skills/beautiful-mermaid/references/mermaid-syntax.md +235 -0
  4. package/.codex/skills/beautiful-mermaid/scripts/create-html.ts +177 -0
  5. package/.codex/skills/beautiful-mermaid/scripts/render.ts +221 -0
  6. package/.codex/skills/find-skills/SKILL.md +133 -0
  7. package/.github/workflows/publish-github-packages.yml +58 -0
  8. package/.github/workflows/publish-npm.yml +46 -0
  9. package/.vscode/settings.json +2 -0
  10. package/AGENTS.md +41 -0
  11. package/LICENSE +21 -0
  12. package/README.md +119 -0
  13. package/bun.lock +327 -0
  14. package/docs/agent/architecture.md +28 -0
  15. package/docs/agent/development_commands.md +6 -0
  16. package/docs/plan/agent-plan-2026-02-05.md +136 -0
  17. package/docs/plan/core-agent-sdk-structure-2026-02-07.md +156 -0
  18. package/docs/plan/implementation-summary.md +303 -0
  19. package/docs/plan/mcp-2026-02-05.md +700 -0
  20. package/docs/plan/op.md +478 -0
  21. package/docs/plan/skills-2026-02-05.md +352 -0
  22. package/docs/plan/skills-flow.svg +120 -0
  23. package/docs/plan/tui-readability-2026-02-06.md +67 -0
  24. package/package.json +34 -0
  25. package/src/cli/index.tsx +4 -0
  26. package/src/cli/main.tsx +98 -0
  27. package/src/core/README.md +19 -0
  28. package/src/core/api/agent.ts +1 -0
  29. package/src/core/api/config.ts +1 -0
  30. package/src/core/api/index.ts +10 -0
  31. package/src/core/api/integrations.ts +1 -0
  32. package/src/core/api/observability.ts +1 -0
  33. package/src/core/api/policy.ts +1 -0
  34. package/src/core/api/providers.ts +1 -0
  35. package/src/core/api/runtime.ts +1 -0
  36. package/src/core/api/shared.ts +1 -0
  37. package/src/core/api/tools.ts +1 -0
  38. package/src/core/api/types.ts +1 -0
  39. package/src/core/index.ts +1 -0
  40. package/src/core/internal/config/defaults.ts +8 -0
  41. package/src/core/internal/config/index.ts +3 -0
  42. package/src/core/internal/config/loader.ts +97 -0
  43. package/src/core/internal/config/schema.ts +47 -0
  44. package/src/core/internal/integrations/index.ts +2 -0
  45. package/src/core/internal/integrations/mcp/connection-manager.ts +231 -0
  46. package/src/core/internal/integrations/mcp/health-checker.ts +91 -0
  47. package/src/core/internal/integrations/mcp/index.ts +197 -0
  48. package/src/core/internal/integrations/mcp/retry-strategy.ts +111 -0
  49. package/src/core/internal/integrations/mcp/tool-cache.ts +103 -0
  50. package/src/core/internal/integrations/mcp/transport.ts +58 -0
  51. package/src/core/internal/integrations/mcp/types.ts +95 -0
  52. package/src/core/internal/integrations/mcp/utils.ts +44 -0
  53. package/src/core/internal/integrations/skills/cache/index.ts +38 -0
  54. package/src/core/internal/integrations/skills/cache/interface.ts +9 -0
  55. package/src/core/internal/integrations/skills/cache/memory-cache.ts +118 -0
  56. package/src/core/internal/integrations/skills/config/defaults.ts +35 -0
  57. package/src/core/internal/integrations/skills/config/index.ts +71 -0
  58. package/src/core/internal/integrations/skills/config/schema.ts +31 -0
  59. package/src/core/internal/integrations/skills/core/errors.ts +36 -0
  60. package/src/core/internal/integrations/skills/core/events.ts +143 -0
  61. package/src/core/internal/integrations/skills/core/types.ts +83 -0
  62. package/src/core/internal/integrations/skills/dependency/conflict-detector.ts +126 -0
  63. package/src/core/internal/integrations/skills/dependency/graph.ts +91 -0
  64. package/src/core/internal/integrations/skills/dependency/resolver.ts +128 -0
  65. package/src/core/internal/integrations/skills/dependency/types.ts +51 -0
  66. package/src/core/internal/integrations/skills/discovery/index.ts +98 -0
  67. package/src/core/internal/integrations/skills/discovery/resolver.ts +39 -0
  68. package/src/core/internal/integrations/skills/discovery/scanner.ts +116 -0
  69. package/src/core/internal/integrations/skills/discovery/strategies/file-system.ts +16 -0
  70. package/src/core/internal/integrations/skills/index.ts +3 -0
  71. package/src/core/internal/integrations/skills/integration/lifecycle.ts +124 -0
  72. package/src/core/internal/integrations/skills/integration/mcp-loader.ts +100 -0
  73. package/src/core/internal/integrations/skills/integration/tool-mapper.ts +56 -0
  74. package/src/core/internal/integrations/skills/loaders/index.ts +5 -0
  75. package/src/core/internal/integrations/skills/loaders/skill-loader.ts +97 -0
  76. package/src/core/internal/integrations/skills/manager.ts +200 -0
  77. package/src/core/internal/integrations/skills/parsers/base.ts +134 -0
  78. package/src/core/internal/integrations/skills/parsers/factory.ts +42 -0
  79. package/src/core/internal/integrations/skills/parsers/index.ts +71 -0
  80. package/src/core/internal/integrations/skills/parsers/markdown.ts +111 -0
  81. package/src/core/internal/integrations/skills/parsers/yaml-metadata.ts +49 -0
  82. package/src/core/internal/integrations/skills/types.ts +15 -0
  83. package/src/core/internal/integrations/skills/utils/fs.ts +59 -0
  84. package/src/core/internal/integrations/skills/utils/logger.ts +109 -0
  85. package/src/core/internal/integrations/skills/utils/path.ts +27 -0
  86. package/src/core/internal/integrations/skills/validation/index.ts +43 -0
  87. package/src/core/internal/integrations/skills/validation/schema.ts +37 -0
  88. package/src/core/internal/integrations/skills/validation/skill-validator.ts +56 -0
  89. package/src/core/internal/observability/index.ts +2 -0
  90. package/src/core/internal/observability/logging/env.ts +32 -0
  91. package/src/core/internal/observability/logging/export.ts +55 -0
  92. package/src/core/internal/observability/logging/index.ts +4 -0
  93. package/src/core/internal/observability/logging/session-logger.ts +54 -0
  94. package/src/core/internal/observability/logging/types.ts +53 -0
  95. package/src/core/internal/policy/index.ts +1 -0
  96. package/src/core/internal/policy/safety/index.ts +2 -0
  97. package/src/core/internal/policy/safety/policy.ts +96 -0
  98. package/src/core/internal/policy/safety/types.ts +24 -0
  99. package/src/core/internal/providers/anthropic/client.ts +20 -0
  100. package/src/core/internal/providers/anthropic/index.ts +1 -0
  101. package/src/core/internal/providers/index.ts +1 -0
  102. package/src/core/internal/sdk/agent/agent.ts +691 -0
  103. package/src/core/internal/sdk/agent/index.ts +3 -0
  104. package/src/core/internal/sdk/agent/session.ts +9 -0
  105. package/src/core/internal/sdk/agent/tool-loop.ts +10 -0
  106. package/src/core/internal/sdk/index.ts +3 -0
  107. package/src/core/internal/sdk/runtime/context.ts +1 -0
  108. package/src/core/internal/sdk/runtime/errors.ts +9 -0
  109. package/src/core/internal/sdk/runtime/execution.ts +12 -0
  110. package/src/core/internal/sdk/runtime/index.ts +3 -0
  111. package/src/core/internal/sdk/types/api.ts +4 -0
  112. package/src/core/internal/sdk/types/index.ts +1 -0
  113. package/src/core/internal/sdk/types/internal.ts +1 -0
  114. package/src/core/internal/shared/fs.ts +10 -0
  115. package/src/core/internal/shared/index.ts +3 -0
  116. package/src/core/internal/shared/message.ts +12 -0
  117. package/src/core/internal/shared/path.ts +10 -0
  118. package/src/core/internal/tools/base/errors.ts +6 -0
  119. package/src/core/internal/tools/base/index.ts +3 -0
  120. package/src/core/internal/tools/base/schema.ts +1 -0
  121. package/src/core/internal/tools/base/tool.ts +42 -0
  122. package/src/core/internal/tools/builtins/architect.ts +45 -0
  123. package/src/core/internal/tools/builtins/bash.ts +135 -0
  124. package/src/core/internal/tools/builtins/fetch.ts +62 -0
  125. package/src/core/internal/tools/builtins/file-edit.ts +134 -0
  126. package/src/core/internal/tools/builtins/file-read.ts +75 -0
  127. package/src/core/internal/tools/builtins/fs.ts +254 -0
  128. package/src/core/internal/tools/builtins/glob.ts +75 -0
  129. package/src/core/internal/tools/builtins/grep.ts +104 -0
  130. package/src/core/internal/tools/builtins/index.ts +26 -0
  131. package/src/core/internal/tools/builtins/list-files.ts +64 -0
  132. package/src/core/internal/tools/builtins/search.ts +50 -0
  133. package/src/core/internal/tools/builtins/skills.ts +127 -0
  134. package/src/core/internal/tools/builtins/todo.ts +121 -0
  135. package/src/core/internal/tools/guards/file-edit-cache.ts +21 -0
  136. package/src/core/internal/tools/guards/limits.ts +43 -0
  137. package/src/core/internal/tools/index.ts +39 -0
  138. package/src/core/internal/tools/registry/index.ts +2 -0
  139. package/src/core/internal/tools/registry/presets.ts +28 -0
  140. package/src/core/internal/tools/registry/registry.ts +21 -0
  141. package/src/index.ts +3 -0
  142. package/src/render/commands/index.ts +113 -0
  143. package/src/render/commands/init.ts +45 -0
  144. package/src/render/components/ActivityPane.tsx +67 -0
  145. package/src/render/components/ChatBubble.tsx +58 -0
  146. package/src/render/components/ConfirmCard.tsx +100 -0
  147. package/src/render/components/ConfirmSelectMenu.tsx +56 -0
  148. package/src/render/components/ConversationPane.tsx +65 -0
  149. package/src/render/components/EventTimeline.tsx +30 -0
  150. package/src/render/components/MarkdownText.tsx +139 -0
  151. package/src/render/components/SlashCommandMenu.tsx +68 -0
  152. package/src/render/components/Spinner.tsx +18 -0
  153. package/src/render/components/StatusBar.tsx +72 -0
  154. package/src/render/components/Timeline.tsx +57 -0
  155. package/src/render/components/TimelineEvent.tsx +313 -0
  156. package/src/render/components/ToolCard.tsx +126 -0
  157. package/src/render/components/formatters/confirm.test.ts +34 -0
  158. package/src/render/components/formatters/confirm.ts +32 -0
  159. package/src/render/index.tsx +466 -0
  160. package/src/render/state/events.ts +301 -0
  161. package/src/render/state/history.ts +5 -0
  162. package/src/render/state/loading.ts +18 -0
  163. package/src/render/state/message.tsx +35 -0
  164. package/src/render/state/store.ts +7 -0
  165. package/src/render/theme.ts +52 -0
  166. package/test-e2e.ts +250 -0
  167. 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
+ }