@supatest/cli 0.0.2 → 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 (75) hide show
  1. package/README.md +58 -315
  2. package/dist/agent-runner.js +224 -52
  3. package/dist/commands/login.js +392 -0
  4. package/dist/commands/setup.js +234 -0
  5. package/dist/config.js +29 -0
  6. package/dist/core/agent.js +270 -0
  7. package/dist/index.js +118 -31
  8. package/dist/modes/headless.js +117 -0
  9. package/dist/modes/interactive.js +430 -0
  10. package/dist/presenters/composite.js +32 -0
  11. package/dist/presenters/console.js +163 -0
  12. package/dist/presenters/react.js +220 -0
  13. package/dist/presenters/types.js +1 -0
  14. package/dist/presenters/web.js +78 -0
  15. package/dist/prompts/builder.js +181 -0
  16. package/dist/prompts/fixer.js +148 -0
  17. package/dist/prompts/headless.md +97 -0
  18. package/dist/prompts/index.js +3 -0
  19. package/dist/prompts/interactive.md +43 -0
  20. package/dist/prompts/plan.md +41 -0
  21. package/dist/prompts/planner.js +70 -0
  22. package/dist/prompts/prompts/builder.md +97 -0
  23. package/dist/prompts/prompts/fixer.md +100 -0
  24. package/dist/prompts/prompts/plan.md +41 -0
  25. package/dist/prompts/prompts/planner.md +41 -0
  26. package/dist/services/api-client.js +244 -0
  27. package/dist/services/event-streamer.js +130 -0
  28. package/dist/ui/App.js +322 -0
  29. package/dist/ui/components/AuthBanner.js +20 -0
  30. package/dist/ui/components/AuthDialog.js +32 -0
  31. package/dist/ui/components/Banner.js +12 -0
  32. package/dist/ui/components/ExpandableSection.js +17 -0
  33. package/dist/ui/components/Header.js +49 -0
  34. package/dist/ui/components/HelpMenu.js +89 -0
  35. package/dist/ui/components/InputPrompt.js +292 -0
  36. package/dist/ui/components/MessageList.js +42 -0
  37. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  38. package/dist/ui/components/Scrollable.js +103 -0
  39. package/dist/ui/components/SessionSelector.js +196 -0
  40. package/dist/ui/components/StatusBar.js +45 -0
  41. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  42. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  43. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  44. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  45. package/dist/ui/components/messages/TodoMessage.js +44 -0
  46. package/dist/ui/components/messages/ToolMessage.js +218 -0
  47. package/dist/ui/components/messages/UserMessage.js +14 -0
  48. package/dist/ui/contexts/KeypressContext.js +527 -0
  49. package/dist/ui/contexts/MouseContext.js +98 -0
  50. package/dist/ui/contexts/SessionContext.js +131 -0
  51. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  52. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  53. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  54. package/dist/ui/hooks/useFocus.js +50 -0
  55. package/dist/ui/hooks/useKeypress.js +26 -0
  56. package/dist/ui/hooks/useModeToggle.js +25 -0
  57. package/dist/ui/types/auth.js +13 -0
  58. package/dist/ui/utils/file-completion.js +56 -0
  59. package/dist/ui/utils/input.js +50 -0
  60. package/dist/ui/utils/markdown.js +376 -0
  61. package/dist/ui/utils/mouse.js +189 -0
  62. package/dist/ui/utils/theme.js +59 -0
  63. package/dist/utils/banner.js +7 -14
  64. package/dist/utils/encryption.js +71 -0
  65. package/dist/utils/events.js +36 -0
  66. package/dist/utils/keychain-storage.js +120 -0
  67. package/dist/utils/logger.js +103 -1
  68. package/dist/utils/node-version.js +1 -3
  69. package/dist/utils/plan-file.js +75 -0
  70. package/dist/utils/project-instructions.js +23 -0
  71. package/dist/utils/rich-logger.js +1 -1
  72. package/dist/utils/stdio.js +80 -0
  73. package/dist/utils/summary.js +1 -5
  74. package/dist/utils/token-storage.js +242 -0
  75. package/package.json +35 -15
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Scrollable wrapper component
3
+ * Provides smooth scrolling with a thin, animated scrollbar
4
+ */
5
+ import { Box, getInnerHeight, getScrollHeight } from "ink";
6
+ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
7
+ import { useMouse } from "../contexts/MouseContext";
8
+ import { useAnimatedScrollbar } from "../hooks/useAnimatedScrollbar";
9
+ import { useBatchedScroll } from "../hooks/useBatchedScroll";
10
+ import { useKeypress } from "../hooks/useKeypress";
11
+ export const Scrollable = ({ children, flexGrow, scrollToBottom, hasFocus = true, }) => {
12
+ const [scrollTop, setScrollTop] = useState(0);
13
+ const ref = useRef(null);
14
+ const [size, setSize] = useState({
15
+ innerHeight: 0,
16
+ scrollHeight: 0,
17
+ });
18
+ const sizeRef = useRef(size);
19
+ useEffect(() => {
20
+ sizeRef.current = size;
21
+ }, [size]);
22
+ const childrenCountRef = useRef(0);
23
+ // Track previous values to avoid unnecessary state updates
24
+ const prevSizeRef = useRef({ innerHeight: 0, scrollHeight: 0 });
25
+ const prevScrollTopRef = useRef(0);
26
+ // Measure container and auto-scroll to bottom when content changes
27
+ // Use a ref to batch updates and avoid cascading re-renders
28
+ useLayoutEffect(() => {
29
+ if (!ref.current) {
30
+ return;
31
+ }
32
+ const innerHeight = Math.round(getInnerHeight(ref.current));
33
+ const scrollHeight = Math.round(getScrollHeight(ref.current));
34
+ const prevSize = prevSizeRef.current;
35
+ const sizeChanged = prevSize.innerHeight !== innerHeight || prevSize.scrollHeight !== scrollHeight;
36
+ // Only update if size actually changed
37
+ if (sizeChanged) {
38
+ prevSizeRef.current = { innerHeight, scrollHeight };
39
+ // Check if we were at bottom before the change (use previous values)
40
+ const wasAtBottom = prevScrollTopRef.current >= prevSize.scrollHeight - prevSize.innerHeight - 1;
41
+ // Batch size and scroll updates together
42
+ const newScrollTop = wasAtBottom ? Math.max(0, scrollHeight - innerHeight) : scrollTop;
43
+ // Only trigger state updates if values actually changed
44
+ if (size.innerHeight !== innerHeight || size.scrollHeight !== scrollHeight) {
45
+ setSize({ innerHeight, scrollHeight });
46
+ }
47
+ if (wasAtBottom && newScrollTop !== scrollTop) {
48
+ prevScrollTopRef.current = newScrollTop;
49
+ setScrollTop(newScrollTop);
50
+ }
51
+ }
52
+ const childCountCurrent = React.Children.count(children);
53
+ // Only auto-scroll to bottom if user was already at bottom or near bottom
54
+ if (scrollToBottom && childrenCountRef.current !== childCountCurrent) {
55
+ const wasAtBottom = scrollTop >= size.scrollHeight - size.innerHeight - 1;
56
+ if (wasAtBottom) {
57
+ const newScrollTop = Math.max(0, scrollHeight - innerHeight);
58
+ if (newScrollTop !== scrollTop) {
59
+ prevScrollTopRef.current = newScrollTop;
60
+ setScrollTop(newScrollTop);
61
+ }
62
+ }
63
+ }
64
+ childrenCountRef.current = childCountCurrent;
65
+ });
66
+ const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
67
+ const scrollBy = useCallback((delta) => {
68
+ const { scrollHeight, innerHeight } = sizeRef.current;
69
+ const current = getScrollTop();
70
+ const next = Math.min(Math.max(0, current + delta), Math.max(0, scrollHeight - innerHeight));
71
+ setPendingScrollTop(next);
72
+ setScrollTop(next);
73
+ }, [getScrollTop, setPendingScrollTop]);
74
+ const { scrollbarColor, scrollByWithAnimation } = useAnimatedScrollbar(hasFocus, scrollBy, {
75
+ focusedColor: "#6C7086", // gemini-cli's text.secondary
76
+ unfocusedColor: "#454759", // interpolated between #6C7086 and #1E1E2E
77
+ });
78
+ // Handle keyboard scrolling with Shift+Up/Down
79
+ // Regular Up/Down is reserved for menu navigation (autocomplete)
80
+ // Uses KeypressContext to filter out mouse events
81
+ useKeypress((key) => {
82
+ if (key.name === 'up' && key.shift) {
83
+ scrollByWithAnimation(-3); // Scroll 3 lines at a time for smoother feel
84
+ }
85
+ else if (key.name === 'down' && key.shift) {
86
+ scrollByWithAnimation(3);
87
+ }
88
+ }, { isActive: hasFocus });
89
+ // Handle mouse/trackpad scrolling
90
+ useMouse((event) => {
91
+ if (event.name === 'scroll-up') {
92
+ scrollByWithAnimation(-3);
93
+ return true; // Mark as handled
94
+ }
95
+ else if (event.name === 'scroll-down') {
96
+ scrollByWithAnimation(3);
97
+ return true; // Mark as handled
98
+ }
99
+ return false;
100
+ }, { isActive: hasFocus });
101
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: flexGrow ?? 1, height: "100%", overflowX: "hidden", overflowY: "scroll", paddingRight: 1, ref: ref, scrollbarThumbColor: scrollbarColor, scrollTop: scrollTop, width: "100%" },
102
+ React.createElement(Box, { flexDirection: "column", flexShrink: 0, width: "100%" }, children)));
103
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Session Selector Component
3
+ * Displays a list of sessions for the user to select from with pagination
4
+ */
5
+ import { Box, Text, useInput } from "ink";
6
+ import React, { useEffect, useState } from "react";
7
+ import { theme } from "../utils/theme.js";
8
+ const PAGE_SIZE = 20;
9
+ const VISIBLE_ITEMS = 10;
10
+ /**
11
+ * Determine if a session is "Me" or "Team" based on authMethod
12
+ */
13
+ function getSessionPrefix(authMethod) {
14
+ return authMethod === "api-key" ? "[Team]" : "[Me]";
15
+ }
16
+ export const SessionSelector = ({ apiClient, onSelect, onCancel, }) => {
17
+ const [allSessions, setAllSessions] = useState([]);
18
+ const [selectedIndex, setSelectedIndex] = useState(0);
19
+ const [isLoading, setIsLoading] = useState(false);
20
+ const [hasMore, setHasMore] = useState(true);
21
+ const [totalSessions, setTotalSessions] = useState(0);
22
+ const [error, setError] = useState(null);
23
+ // Load initial page
24
+ useEffect(() => {
25
+ loadMoreSessions();
26
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
27
+ const loadMoreSessions = async () => {
28
+ if (isLoading || !hasMore) {
29
+ return;
30
+ }
31
+ setIsLoading(true);
32
+ setError(null);
33
+ try {
34
+ const offset = allSessions.length;
35
+ const result = await apiClient.getSessions(PAGE_SIZE, offset);
36
+ const newSessions = result.sessions.map((s) => ({
37
+ session: s,
38
+ prefix: getSessionPrefix(s.authMethod),
39
+ }));
40
+ setTotalSessions(result.pagination.total);
41
+ setHasMore(offset + result.sessions.length < result.pagination.total);
42
+ setAllSessions((prev) => [...prev, ...newSessions]);
43
+ }
44
+ catch (err) {
45
+ setError(err instanceof Error ? err.message : String(err));
46
+ setHasMore(false);
47
+ }
48
+ finally {
49
+ setIsLoading(false);
50
+ }
51
+ };
52
+ useInput((input, key) => {
53
+ if (allSessions.length === 0) {
54
+ if (key.escape || input === "q") {
55
+ onCancel();
56
+ }
57
+ return;
58
+ }
59
+ if (key.upArrow) {
60
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : allSessions.length - 1));
61
+ }
62
+ else if (key.downArrow) {
63
+ const newIndex = selectedIndex < allSessions.length - 1 ? selectedIndex + 1 : 0;
64
+ setSelectedIndex(newIndex);
65
+ // Load more when approaching the end
66
+ if (newIndex >= allSessions.length - 3 && hasMore && !isLoading) {
67
+ loadMoreSessions();
68
+ }
69
+ }
70
+ else if (key.return) {
71
+ if (allSessions[selectedIndex]) {
72
+ onSelect(allSessions[selectedIndex].session);
73
+ }
74
+ }
75
+ else if (key.escape || input === "q") {
76
+ onCancel();
77
+ }
78
+ });
79
+ if (error) {
80
+ return (React.createElement(Box, { borderColor: "red", borderStyle: "round", flexDirection: "column", padding: 1 },
81
+ React.createElement(Text, { bold: true, color: "red" }, "Error Loading Sessions"),
82
+ React.createElement(Text, { color: theme.text.dim }, error),
83
+ React.createElement(Box, { marginTop: 1 },
84
+ React.createElement(Text, { color: theme.text.dim },
85
+ "Press ",
86
+ React.createElement(Text, { bold: true }, "ESC"),
87
+ " to cancel"))));
88
+ }
89
+ if (allSessions.length === 0 && isLoading) {
90
+ return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 },
91
+ React.createElement(Text, { bold: true, color: "cyan" }, "Loading Sessions..."),
92
+ React.createElement(Text, { color: theme.text.dim }, "Fetching your sessions from the server")));
93
+ }
94
+ if (allSessions.length === 0 && !isLoading) {
95
+ return (React.createElement(Box, { borderColor: "yellow", borderStyle: "round", flexDirection: "column", padding: 1 },
96
+ React.createElement(Text, { bold: true, color: "yellow" }, "No Sessions Found"),
97
+ React.createElement(Text, { color: theme.text.dim }, "No previous sessions available. Start a new conversation!"),
98
+ React.createElement(Box, { marginTop: 1 },
99
+ React.createElement(Text, { color: theme.text.dim },
100
+ "Press ",
101
+ React.createElement(Text, { bold: true }, "ESC"),
102
+ " to cancel"))));
103
+ }
104
+ // Calculate the visible window of sessions
105
+ const VISIBLE_ITEMS = 10;
106
+ // Calculate start and end indices for the visible window
107
+ // Always try to center the selected item, but adjust for boundaries
108
+ let startIndex;
109
+ let endIndex;
110
+ if (allSessions.length <= VISIBLE_ITEMS) {
111
+ // Show all sessions if we have fewer than the visible limit
112
+ startIndex = 0;
113
+ endIndex = allSessions.length;
114
+ }
115
+ else {
116
+ // Try to center the selected item
117
+ const halfWindow = Math.floor(VISIBLE_ITEMS / 2);
118
+ startIndex = Math.max(0, selectedIndex - halfWindow);
119
+ endIndex = startIndex + VISIBLE_ITEMS;
120
+ // If we're near the end, adjust to show the last N items
121
+ if (endIndex > allSessions.length) {
122
+ endIndex = allSessions.length;
123
+ startIndex = Math.max(0, endIndex - VISIBLE_ITEMS);
124
+ }
125
+ }
126
+ const visibleSessions = allSessions.slice(startIndex, endIndex);
127
+ // Calculate max title length for proper alignment
128
+ const MAX_TITLE_WIDTH = 50;
129
+ const PREFIX_WIDTH = 6; // "[Me] " or "[Team] "
130
+ return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 },
131
+ React.createElement(Box, { marginBottom: 1 },
132
+ React.createElement(Text, { bold: true, color: "cyan" }, "Select a Session to Resume")),
133
+ React.createElement(Box, { flexDirection: "column" }, visibleSessions.map((item, index) => {
134
+ const actualIndex = startIndex + index;
135
+ const isSelected = actualIndex === selectedIndex;
136
+ const title = item.session.title || "Untitled session";
137
+ // Truncate and pad title to fixed width for alignment
138
+ let displayTitle = title.length > MAX_TITLE_WIDTH
139
+ ? `${title.slice(0, MAX_TITLE_WIDTH - 3)}...`
140
+ : title;
141
+ // Pad the title to fixed width
142
+ displayTitle = displayTitle.padEnd(MAX_TITLE_WIDTH, " ");
143
+ // Format date more compactly
144
+ const sessionDate = new Date(item.session.createdAt);
145
+ const now = new Date();
146
+ const isToday = sessionDate.toDateString() === now.toDateString();
147
+ const yesterday = new Date(now);
148
+ yesterday.setDate(yesterday.getDate() - 1);
149
+ const isYesterday = sessionDate.toDateString() === yesterday.toDateString();
150
+ let dateStr;
151
+ if (isToday) {
152
+ dateStr = `Today ${sessionDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`;
153
+ }
154
+ else if (isYesterday) {
155
+ dateStr = `Yesterday ${sessionDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`;
156
+ }
157
+ else {
158
+ dateStr = `${sessionDate.toLocaleDateString([], { month: "numeric", day: "numeric" })} ${sessionDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`;
159
+ }
160
+ const prefix = item.prefix.padEnd(PREFIX_WIDTH, " "); // Pad prefix to fixed width
161
+ const prefixColor = item.prefix === "[Me]" ? "cyan" : "yellow";
162
+ const indicator = isSelected ? "▶ " : " ";
163
+ const bgColor = isSelected ? theme.text.accent : undefined;
164
+ return (React.createElement(Box, { key: item.session.id, width: "100%" },
165
+ React.createElement(Text, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, indicator),
166
+ React.createElement(Text, { backgroundColor: bgColor, bold: isSelected, color: prefixColor }, prefix),
167
+ React.createElement(Text, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, displayTitle),
168
+ React.createElement(Text, { backgroundColor: bgColor, color: theme.text.dim },
169
+ "(",
170
+ dateStr,
171
+ ")",
172
+ "\n")));
173
+ })),
174
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 },
175
+ (allSessions.length > VISIBLE_ITEMS || totalSessions > allSessions.length) && (React.createElement(Box, { marginBottom: 1 },
176
+ React.createElement(Text, { color: "yellow" },
177
+ "Showing ",
178
+ startIndex + 1,
179
+ "-",
180
+ endIndex,
181
+ " of ",
182
+ totalSessions || allSessions.length,
183
+ " sessions",
184
+ hasMore && !isLoading && React.createElement(Text, { color: theme.text.dim }, " \u2022 Scroll for more")))),
185
+ React.createElement(Box, null,
186
+ React.createElement(Text, { color: theme.text.dim },
187
+ "Use ",
188
+ React.createElement(Text, { bold: true }, "\u2191\u2193"),
189
+ " to navigate \u2022 ",
190
+ React.createElement(Text, { bold: true }, "Enter"),
191
+ " to select \u2022 ",
192
+ React.createElement(Text, { bold: true }, "ESC"),
193
+ " to cancel")),
194
+ isLoading && (React.createElement(Box, { marginTop: 1 },
195
+ React.createElement(Text, { color: "cyan" }, "Loading more sessions..."))))));
196
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Status Bar Component
3
+ * Displays session stats, web URL, and agent status
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { useSession } from "../contexts/SessionContext.js";
8
+ import { theme } from "../utils/theme.js";
9
+ function formatTokens(tokens) {
10
+ if (tokens >= 1000000)
11
+ return `${(tokens / 1000000).toFixed(1)}M`;
12
+ if (tokens >= 1000)
13
+ return `${(tokens / 1000).toFixed(1)}k`;
14
+ return tokens.toString();
15
+ }
16
+ export const StatusBar = () => {
17
+ const { stats, webUrl, isAgentRunning } = useSession();
18
+ const elapsedSeconds = Math.floor((Date.now() - stats.startTime) / 1000);
19
+ const minutes = Math.floor(elapsedSeconds / 60);
20
+ const seconds = elapsedSeconds % 60;
21
+ const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
22
+ return (React.createElement(Box, { borderColor: theme.border.default, borderStyle: "round", flexDirection: "column", paddingX: 1 },
23
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between" },
24
+ React.createElement(Box, null,
25
+ React.createElement(Text, { color: theme.text.dim },
26
+ "Files: ",
27
+ React.createElement(Text, { color: theme.text.accent }, stats.filesModified.size)),
28
+ React.createElement(Text, { color: theme.text.dim }, " \u2022 "),
29
+ React.createElement(Text, { color: theme.text.dim },
30
+ "Commands: ",
31
+ React.createElement(Text, { color: theme.text.accent }, stats.commandsRun.length)),
32
+ React.createElement(Text, { color: theme.text.dim }, " \u2022 "),
33
+ React.createElement(Text, { color: theme.text.dim },
34
+ "Time: ",
35
+ React.createElement(Text, { color: theme.text.accent }, timeStr)),
36
+ React.createElement(Text, { color: theme.text.dim }, " \u2022 "),
37
+ React.createElement(Text, { color: theme.text.dim },
38
+ "Tokens: ",
39
+ React.createElement(Text, { color: theme.text.accent }, formatTokens(stats.totalTokens)))),
40
+ React.createElement(Box, null, isAgentRunning ? (React.createElement(Text, { color: theme.status.inProgress }, "\u25CF Running")) : (React.createElement(Text, { color: theme.status.completed }, "\u25CF Ready")))),
41
+ webUrl && (React.createElement(Box, { marginTop: 0 },
42
+ React.createElement(Text, { color: theme.text.dim },
43
+ "Web: ",
44
+ React.createElement(Text, { color: theme.text.info, underline: true }, webUrl))))));
45
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Assistant message component
3
+ * Displays AI responses with markdown rendering
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import Spinner from "ink-spinner";
7
+ import React from "react";
8
+ import { MarkdownDisplay } from "../../utils/markdown.js";
9
+ import { theme } from "../../utils/theme.js";
10
+ export const AssistantMessage = ({ text, isPending = false, terminalWidth = 80, }) => {
11
+ // Use "dots" spinner to match the screenshot look (animating dots)
12
+ // When pending, show the spinner. When done, show the static accent icon.
13
+ const prefix = isPending ? React.createElement(Spinner, { type: "dots" }) : "✦ ";
14
+ const prefixWidth = 2;
15
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
16
+ React.createElement(Box, { width: prefixWidth },
17
+ React.createElement(Text, { color: isPending ? theme.text.dim : theme.text.accent }, prefix)),
18
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
19
+ React.createElement(MarkdownDisplay, { isPending: isPending, terminalWidth: terminalWidth - prefixWidth, text: text }))));
20
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Error message component
3
+ * Displays errors and warnings
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const ErrorMessage = ({ message, type = "error", }) => {
9
+ const { icon, color } = getErrorStyle(type);
10
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
11
+ React.createElement(Text, { color: color },
12
+ icon,
13
+ " ",
14
+ message)));
15
+ };
16
+ /**
17
+ * Get icon and color based on error type
18
+ */
19
+ function getErrorStyle(type) {
20
+ const styles = {
21
+ error: { icon: "✗", color: theme.text.error },
22
+ warning: { icon: "⚠", color: theme.text.warning },
23
+ info: { icon: "ℹ", color: theme.text.info },
24
+ };
25
+ return styles[type];
26
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Loading message component
3
+ * Displays a spinner with static text while waiting for assistant response
4
+ * Note: We avoid text rotation to prevent layout shifts and flickering
5
+ */
6
+ import { Box, Text } from "ink";
7
+ import Spinner from "ink-spinner";
8
+ import React, { useMemo } from "react";
9
+ import { theme } from "../../utils/theme.js";
10
+ // Fun loading messages - pick one randomly on mount (no rotation to avoid flicker)
11
+ const LOADING_MESSAGES = [
12
+ "Thinking...",
13
+ "Working...",
14
+ "Processing...",
15
+ ];
16
+ export const LoadingMessage = () => {
17
+ // Pick a random message once on mount - no rotation to prevent layout shifts
18
+ const message = useMemo(() => LOADING_MESSAGES[Math.floor(Math.random() * LOADING_MESSAGES.length)], []);
19
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
20
+ React.createElement(Box, { width: 2 },
21
+ React.createElement(Text, { color: theme.text.accent },
22
+ React.createElement(Spinner, { type: "dots" }))),
23
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
24
+ React.createElement(Text, { color: theme.text.dim },
25
+ message,
26
+ " ",
27
+ React.createElement(Text, { color: theme.text.dim }, "(esc to interrupt)")))));
28
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Thinking message component
3
+ * Displays Claude's thinking process (collapsible by default)
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const ThinkingMessage = ({ id, content, isExpanded = false, onToggle, }) => {
9
+ return (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
10
+ React.createElement(Box, { flexDirection: "row" },
11
+ React.createElement(Text, { color: theme.text.dim }, "\u25CF Thinking"),
12
+ content && (React.createElement(Text, { color: theme.text.dim },
13
+ " ",
14
+ isExpanded ? "▼" : "▶"))),
15
+ isExpanded && content && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
16
+ React.createElement(Text, { color: theme.text.dim }, content)))));
17
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Todo message component
3
+ * Displays todo list with progress
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const TodoMessage = ({ todos }) => {
9
+ const completed = todos.filter((t) => t.status === "completed");
10
+ const inProgress = todos.filter((t) => t.status === "in_progress");
11
+ const pending = todos.filter((t) => t.status === "pending");
12
+ const total = todos.length;
13
+ const completedCount = completed.length;
14
+ const progress = total > 0 ? Math.round((completedCount / total) * 100) : 0;
15
+ // Progress bar
16
+ const barLength = 20;
17
+ const filledLength = Math.round((barLength * completedCount) / total);
18
+ const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
19
+ return (React.createElement(Box, { flexDirection: "column", marginY: 0 },
20
+ React.createElement(Box, { flexDirection: "row" },
21
+ React.createElement(Text, { color: theme.text.info }, "\uD83D\uDCDD "),
22
+ React.createElement(Text, { color: theme.text.dim }, "Todo Progress: "),
23
+ React.createElement(Text, { color: theme.text.accent }, bar),
24
+ React.createElement(Text, { color: theme.text.primary },
25
+ " ",
26
+ progress,
27
+ "%"),
28
+ React.createElement(Text, { color: theme.text.dim },
29
+ " ",
30
+ "(",
31
+ completedCount,
32
+ "/",
33
+ total,
34
+ ")")),
35
+ inProgress.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, inProgress.map((todo, idx) => (React.createElement(Box, { flexDirection: "row", key: `in-progress-${idx}` },
36
+ React.createElement(Text, { color: theme.status.inProgress }, "\u2192 "),
37
+ React.createElement(Text, { color: theme.text.primary }, todo.activeForm || todo.content)))))),
38
+ completed.length > 0 && completed.length <= 2 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, completed.map((todo, idx) => (React.createElement(Box, { flexDirection: "row", key: `completed-${idx}` },
39
+ React.createElement(Text, { color: theme.status.completed }, "\u2713 "),
40
+ React.createElement(Text, { color: theme.text.dim }, todo.content)))))),
41
+ pending.length > 0 && pending.length <= 3 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, pending.map((todo, idx) => (React.createElement(Box, { flexDirection: "row", key: `pending-${idx}` },
42
+ React.createElement(Text, { color: theme.status.pending }, "\u23F3 "),
43
+ React.createElement(Text, { color: theme.text.secondary }, todo.content))))))));
44
+ };