@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,20 @@
1
+ /**
2
+ * Auth Banner Component
3
+ * Shows authentication status when user is not logged in
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { AuthState } from "../types/auth.js";
8
+ import { theme } from "../utils/theme.js";
9
+ export const AuthBanner = ({ authState }) => {
10
+ // Don't show banner if authenticated
11
+ if (authState === AuthState.Authenticated) {
12
+ return null;
13
+ }
14
+ const message = authState === AuthState.Authenticating
15
+ ? "Authenticating..."
16
+ : "Not logged in. Type /login to authenticate.";
17
+ const color = authState === AuthState.Authenticating ? theme.text.info : theme.text.warning;
18
+ return (React.createElement(Box, { marginBottom: 0, paddingX: 1 },
19
+ React.createElement(Text, { color: color }, message)));
20
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Auth Dialog Component
3
+ * Prompts user to login (authentication is required)
4
+ *
5
+ * Based on Gemini CLI (Apache 2.0 License)
6
+ * https://github.com/google-gemini/gemini-cli
7
+ * Copyright 2025 Google LLC
8
+ */
9
+ import { Box, Text } from "ink";
10
+ import React from "react";
11
+ import { useKeypress } from "../hooks/useKeypress.js";
12
+ import { theme } from "../utils/theme.js";
13
+ export const AuthDialog = ({ onLogin }) => {
14
+ useKeypress((key) => {
15
+ // Any key press triggers login (Enter, L, or any other key)
16
+ if (key.name === "return" || key.name === "l" || key.sequence === "l") {
17
+ onLogin();
18
+ }
19
+ }, { isActive: true });
20
+ return (React.createElement(Box, { borderColor: theme.border.accent, borderStyle: "round", flexDirection: "column", paddingX: 2, paddingY: 1 },
21
+ React.createElement(Text, { bold: true, color: theme.text.primary }, "Welcome to Supatest CLI"),
22
+ React.createElement(Box, { marginTop: 1 },
23
+ React.createElement(Text, { color: theme.text.secondary }, "Authentication is required to use Supatest CLI.")),
24
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 },
25
+ React.createElement(Box, null,
26
+ React.createElement(Text, { color: theme.text.accent },
27
+ ">",
28
+ " "),
29
+ React.createElement(Text, { color: theme.text.primary }, "[L] Login with browser"))),
30
+ React.createElement(Box, { marginTop: 1 },
31
+ React.createElement(Text, { color: theme.text.dim, italic: true }, "Press Enter or L to login"))));
32
+ };
@@ -0,0 +1,12 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { theme } from "../utils/theme.js";
4
+ export const Banner = ({ text, type = "info" }) => {
5
+ let borderColor = theme.border.default;
6
+ if (type === "warning")
7
+ borderColor = theme.text.warning;
8
+ if (type === "error")
9
+ borderColor = theme.border.error;
10
+ return (React.createElement(Box, { borderColor: borderColor, borderStyle: "round", flexDirection: "column", marginBottom: 1, paddingX: 1, width: "100%" },
11
+ React.createElement(Text, { color: type === "warning" ? theme.text.warning : type === "error" ? theme.text.error : theme.text.primary }, text)));
12
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Expandable Section Component
3
+ * Shows a collapsible section with summary and expandable details
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../utils/theme.js";
8
+ export const ExpandableSection = ({ id, summary, summaryColor = theme.text.dim, details, isExpanded = false, onToggle, }) => {
9
+ return (React.createElement(Box, { flexDirection: "column" },
10
+ React.createElement(Box, { flexDirection: "row" },
11
+ React.createElement(Text, { color: summaryColor }, summary),
12
+ details && (React.createElement(Text, { color: theme.text.dim },
13
+ " ",
14
+ isExpanded ? "▼" : "▶"))),
15
+ isExpanded && details && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
16
+ React.createElement(Text, { color: theme.text.dim }, details)))));
17
+ };
@@ -0,0 +1,49 @@
1
+ import { Box, Text } from "ink";
2
+ import Gradient from "ink-gradient";
3
+ import React from "react";
4
+ import { getBanner } from "../../utils/banner.js";
5
+ import { theme } from "../utils/theme.js";
6
+ export const Header = ({ version, currentFolder, gitBranch }) => {
7
+ const banner = getBanner();
8
+ // Build the info line: "v0.0.2 • ~/path/to/folder on branch"
9
+ const infoParts = [`v${version}`];
10
+ if (currentFolder) {
11
+ let locationStr = currentFolder;
12
+ if (gitBranch) {
13
+ locationStr += ` on ${gitBranch}`;
14
+ }
15
+ infoParts.push(locationStr);
16
+ }
17
+ const infoLine = infoParts.join(" • ");
18
+ return (React.createElement(Box, { alignItems: "center", flexDirection: "column", marginBottom: 1, marginTop: 5, width: "100%" },
19
+ React.createElement(Gradient, { colors: ["#C96868", "#FF8C94"] },
20
+ React.createElement(Text, null, banner)),
21
+ React.createElement(Box, { justifyContent: "center", marginTop: 0 },
22
+ React.createElement(Text, { color: theme.text.dim }, infoLine)),
23
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, paddingX: 2, width: "100%" },
24
+ React.createElement(Box, { flexDirection: "column", marginBottom: 0 },
25
+ React.createElement(Text, { color: theme.text.dim },
26
+ "\uD83D\uDCA1 ",
27
+ React.createElement(Text, { color: theme.text.secondary }, "Tip:"),
28
+ " Use ",
29
+ React.createElement(Text, { bold: true, color: theme.text.accent }, "@filename"),
30
+ " to reference files, or ",
31
+ React.createElement(Text, { bold: true, color: theme.text.accent }, "/help"),
32
+ " for commands")),
33
+ React.createElement(Box, { flexDirection: "column", marginTop: 0 },
34
+ React.createElement(Text, { color: theme.text.dim },
35
+ "\u2328\uFE0F ",
36
+ React.createElement(Text, { color: theme.text.secondary }, "Shortcuts:"),
37
+ " ",
38
+ React.createElement(Text, { bold: true, color: theme.text.accent }, "Ctrl+H"),
39
+ " help, ",
40
+ React.createElement(Text, { bold: true, color: theme.text.accent }, "Ctrl+C"),
41
+ " exit, ",
42
+ React.createElement(Text, { bold: true, color: theme.text.accent }, "ESC"),
43
+ " interrupt")),
44
+ React.createElement(Box, { flexDirection: "column", marginTop: 0 },
45
+ React.createElement(Text, { color: theme.text.dim },
46
+ "\uD83D\uDE80 ",
47
+ React.createElement(Text, { color: theme.text.secondary }, "Prompt Tips:"),
48
+ " Be explicit with instructions, provide context, use examples, and think step-by-step")))));
49
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Help Menu Component
3
+ * Displays available commands, keyboard shortcuts, and usage tips
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../utils/theme.js";
8
+ export const HelpMenu = ({ isAuthenticated }) => {
9
+ return (React.createElement(Box, { borderColor: theme.border.accent, borderStyle: "round", flexDirection: "column", marginBottom: 1, marginTop: 1, paddingX: 2, paddingY: 1 },
10
+ React.createElement(Text, { bold: true, color: theme.text.accent }, "\uD83D\uDCD6 Supatest AI CLI - Help"),
11
+ React.createElement(Box, { marginTop: 1 }),
12
+ React.createElement(Text, { bold: true, color: theme.text.secondary }, "Slash Commands:"),
13
+ React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0 },
14
+ React.createElement(Text, null,
15
+ React.createElement(Text, { color: theme.text.accent }, "/help"),
16
+ React.createElement(Text, { color: theme.text.dim }, " or "),
17
+ React.createElement(Text, { color: theme.text.accent }, "/?"),
18
+ React.createElement(Text, { color: theme.text.dim }, " - Toggle this help menu")),
19
+ React.createElement(Text, null,
20
+ React.createElement(Text, { color: theme.text.accent }, "/resume"),
21
+ React.createElement(Text, { color: theme.text.dim }, " - Resume a previous session")),
22
+ React.createElement(Text, null,
23
+ React.createElement(Text, { color: theme.text.accent }, "/clear"),
24
+ React.createElement(Text, { color: theme.text.dim }, " - Clear message history")),
25
+ React.createElement(Text, null,
26
+ React.createElement(Text, { color: theme.text.accent }, "/setup"),
27
+ React.createElement(Text, { color: theme.text.dim }, " - Initial setup for Supatest CLI")),
28
+ isAuthenticated ? (React.createElement(Text, null,
29
+ React.createElement(Text, { color: theme.text.accent }, "/logout"),
30
+ React.createElement(Text, { color: theme.text.dim }, " - Log out of Supatest"))) : (React.createElement(Text, null,
31
+ React.createElement(Text, { color: theme.text.accent }, "/login"),
32
+ React.createElement(Text, { color: theme.text.dim }, " - Authenticate with Supatest"))),
33
+ React.createElement(Text, null,
34
+ React.createElement(Text, { color: theme.text.accent }, "/exit"),
35
+ React.createElement(Text, { color: theme.text.dim }, " - Exit the CLI"))),
36
+ React.createElement(Box, { marginTop: 1 }),
37
+ React.createElement(Text, { bold: true, color: theme.text.secondary }, "Keyboard Shortcuts:"),
38
+ React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0 },
39
+ React.createElement(Text, null,
40
+ React.createElement(Text, { color: theme.text.accent }, "?"),
41
+ React.createElement(Text, { color: theme.text.dim },
42
+ " ",
43
+ "- Toggle help (when input is empty)")),
44
+ React.createElement(Text, null,
45
+ React.createElement(Text, { color: theme.text.accent }, "Ctrl+H"),
46
+ React.createElement(Text, { color: theme.text.dim }, " - Toggle help")),
47
+ React.createElement(Text, null,
48
+ React.createElement(Text, { color: theme.text.accent }, "Ctrl+C"),
49
+ React.createElement(Text, { color: theme.text.dim },
50
+ " ",
51
+ "- Exit (or clear input if not empty)")),
52
+ React.createElement(Text, null,
53
+ React.createElement(Text, { color: theme.text.accent }, "Ctrl+D"),
54
+ React.createElement(Text, { color: theme.text.dim }, " - Exit immediately")),
55
+ React.createElement(Text, null,
56
+ React.createElement(Text, { color: theme.text.accent }, "Ctrl+L"),
57
+ React.createElement(Text, { color: theme.text.dim }, " - Clear terminal screen")),
58
+ React.createElement(Text, null,
59
+ React.createElement(Text, { color: theme.text.accent }, "Ctrl+U"),
60
+ React.createElement(Text, { color: theme.text.dim }, " - Clear current input line")),
61
+ React.createElement(Text, null,
62
+ React.createElement(Text, { color: theme.text.accent }, "ESC"),
63
+ React.createElement(Text, { color: theme.text.dim }, " - Interrupt running agent")),
64
+ React.createElement(Text, null,
65
+ React.createElement(Text, { color: theme.text.accent }, "Shift+Up/Down"),
66
+ React.createElement(Text, { color: theme.text.dim }, " - Scroll through messages")),
67
+ React.createElement(Text, null,
68
+ React.createElement(Text, { color: theme.text.accent }, "Shift+Enter"),
69
+ React.createElement(Text, { color: theme.text.dim }, " - Add new line in input")),
70
+ React.createElement(Text, null,
71
+ React.createElement(Text, { color: theme.text.accent }, "ctrl+o"),
72
+ React.createElement(Text, { color: theme.text.dim }, " - Toggle tool outputs"))),
73
+ React.createElement(Box, { marginTop: 1 }),
74
+ React.createElement(Text, { bold: true, color: theme.text.secondary }, "File References:"),
75
+ React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0 },
76
+ React.createElement(Text, null,
77
+ React.createElement(Text, { color: theme.text.accent }, "@filename"),
78
+ React.createElement(Text, { color: theme.text.dim },
79
+ " ",
80
+ "- Reference a file (autocomplete with Tab)")),
81
+ React.createElement(Text, { color: theme.text.dim }, "Example: \"Fix the bug in @src/app.ts\"")),
82
+ React.createElement(Box, { marginTop: 1 }),
83
+ React.createElement(Text, { bold: true, color: theme.text.secondary }, "Tips:"),
84
+ React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0 },
85
+ React.createElement(Text, { color: theme.text.dim }, "\u2022 Press Enter to submit your task"),
86
+ React.createElement(Text, { color: theme.text.dim }, "\u2022 Use Shift+Enter to write multi-line prompts"),
87
+ React.createElement(Text, { color: theme.text.dim }, "\u2022 Drag and drop files into the terminal to add file paths"),
88
+ React.createElement(Text, { color: theme.text.dim }, "\u2022 The agent will automatically run tools and fix issues"))));
89
+ };
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Input Prompt Component
3
+ * Multi-line input field for entering tasks
4
+ * Supports:
5
+ * - File autocompletion (@filename)
6
+ * - Slash commands (handled by parent)
7
+ */
8
+ import path from "node:path";
9
+ import chalk from "chalk";
10
+ import { Box, Text } from "ink";
11
+ import React, { forwardRef, useEffect, useImperativeHandle, useState } from "react";
12
+ import { useSession } from "../contexts/SessionContext.js";
13
+ import { useKeypress } from "../hooks/useKeypress.js";
14
+ import { getFiles } from "../utils/file-completion.js";
15
+ import { theme } from "../utils/theme.js";
16
+ export const InputPrompt = forwardRef(({ onSubmit, placeholder = "Enter your task (press Enter to submit, Shift+Enter for new line)...", disabled = false, onHelpToggle, currentFolder, gitBranch, onInputChange, }, ref) => {
17
+ const { messages, agentMode, stats } = useSession();
18
+ const [value, setValue] = useState("");
19
+ const [cursorOffset, setCursorOffset] = useState(0);
20
+ // Autocomplete State
21
+ const [allFiles, setAllFiles] = useState([]);
22
+ const [suggestions, setSuggestions] = useState([]);
23
+ const [activeSuggestion, setActiveSuggestion] = useState(0);
24
+ const [showSuggestions, setShowSuggestions] = useState(false);
25
+ const [mentionStartIndex, setMentionStartIndex] = useState(-1);
26
+ // Slash Command State
27
+ const SLASH_COMMANDS = [
28
+ { name: "/help", desc: "Show help" },
29
+ { name: "/resume", desc: "Resume session" },
30
+ { name: "/clear", desc: "Clear history" },
31
+ { name: "/setup", desc: "Install Playwright browsers" },
32
+ { name: "/login", desc: "Authenticate with Supatest" },
33
+ { name: "/logout", desc: "Log out" },
34
+ { name: "/exit", desc: "Exit CLI" }
35
+ ];
36
+ const [isSlashCommand, setIsSlashCommand] = useState(false);
37
+ useImperativeHandle(ref, () => ({
38
+ clear: () => {
39
+ setValue("");
40
+ setCursorOffset(0);
41
+ setShowSuggestions(false);
42
+ onInputChange?.("");
43
+ }
44
+ }));
45
+ // Load files on mount
46
+ useEffect(() => {
47
+ // Load files asynchronously to avoid blocking initial render
48
+ setTimeout(() => {
49
+ try {
50
+ const files = getFiles();
51
+ setAllFiles(files);
52
+ }
53
+ catch (e) {
54
+ // Ignore errors
55
+ }
56
+ }, 100);
57
+ }, []);
58
+ const updateValue = (newValue, newCursor) => {
59
+ setValue(newValue);
60
+ setCursorOffset(newCursor);
61
+ checkSuggestions(newValue, newCursor);
62
+ onInputChange?.(newValue);
63
+ };
64
+ const checkSuggestions = (text, cursor) => {
65
+ // 1. Check for Slash Commands (must be at start)
66
+ if (text.startsWith("/") && cursor <= text.length && !text.includes(" ", 1)) {
67
+ const query = text.slice(1); // Remove '/'
68
+ const matches = SLASH_COMMANDS
69
+ .filter(cmd => cmd.name.slice(1).startsWith(query.toLowerCase()))
70
+ .map(cmd => `${cmd.name} ${cmd.desc}`);
71
+ if (matches.length > 0) {
72
+ setSuggestions(matches);
73
+ setShowSuggestions(true);
74
+ setActiveSuggestion(0);
75
+ setIsSlashCommand(true);
76
+ return;
77
+ }
78
+ }
79
+ setIsSlashCommand(false);
80
+ // 2. Check for File Mentions
81
+ const textBeforeCursor = text.slice(0, cursor);
82
+ const lastAt = textBeforeCursor.lastIndexOf("@");
83
+ if (lastAt !== -1) {
84
+ // Check if @ is at start or preceded by whitespace
85
+ const isValidStart = lastAt === 0 || /\s/.test(textBeforeCursor[lastAt - 1]);
86
+ if (isValidStart) {
87
+ const query = textBeforeCursor.slice(lastAt + 1);
88
+ // Stop suggestions if query contains whitespace (end of mention)
89
+ if (!/\s/.test(query)) {
90
+ const matches = allFiles
91
+ .filter((f) => f.toLowerCase().includes(query.toLowerCase()))
92
+ .slice(0, 5); // Limit to 5 suggestions
93
+ if (matches.length > 0) {
94
+ setSuggestions(matches);
95
+ setShowSuggestions(true);
96
+ setActiveSuggestion(0);
97
+ setMentionStartIndex(lastAt);
98
+ return;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ setShowSuggestions(false);
104
+ };
105
+ const completeSuggestion = (submit = false) => {
106
+ if (!showSuggestions || suggestions.length === 0)
107
+ return;
108
+ // Handle Slash Command Completion
109
+ if (isSlashCommand) {
110
+ const selectedCmd = suggestions[activeSuggestion].split(" ")[0];
111
+ if (submit) {
112
+ onSubmit(selectedCmd);
113
+ setValue("");
114
+ setCursorOffset(0);
115
+ setShowSuggestions(false);
116
+ onInputChange?.("");
117
+ return;
118
+ }
119
+ updateValue(selectedCmd, selectedCmd.length);
120
+ setShowSuggestions(false);
121
+ return;
122
+ }
123
+ const selectedFile = suggestions[activeSuggestion];
124
+ const textBeforeMention = value.slice(0, mentionStartIndex);
125
+ const textAfterCursor = value.slice(cursorOffset);
126
+ // Add @ + filename + space
127
+ const newValue = textBeforeMention + "@" + selectedFile + " " + textAfterCursor;
128
+ const newCursor = mentionStartIndex + 1 + selectedFile.length + 1;
129
+ updateValue(newValue, newCursor);
130
+ setShowSuggestions(false);
131
+ };
132
+ useKeypress((key) => {
133
+ if (disabled)
134
+ return;
135
+ const input = key.sequence;
136
+ // Mouse events are now filtered by KeypressContext
137
+ // So we don't need to check for them here
138
+ // Handle Paste / Drag & Drop (file paths)
139
+ if (input.length > 1 && (input.includes("/") || input.includes("\\"))) {
140
+ let cleanPath = input.trim();
141
+ // Remove surrounding quotes
142
+ if ((cleanPath.startsWith('"') && cleanPath.endsWith('"')) ||
143
+ (cleanPath.startsWith("'") && cleanPath.endsWith("'"))) {
144
+ cleanPath = cleanPath.slice(1, -1);
145
+ }
146
+ // Remove escaped spaces
147
+ cleanPath = cleanPath.replace(/\\ /g, " ");
148
+ // If it looks like an absolute path, treat as file drop
149
+ if (path.isAbsolute(cleanPath)) {
150
+ try {
151
+ const cwd = process.cwd();
152
+ const rel = path.relative(cwd, cleanPath);
153
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
154
+ cleanPath = rel;
155
+ }
156
+ }
157
+ catch (e) {
158
+ // Ignore
159
+ }
160
+ const charBefore = value[cursorOffset - 1];
161
+ const prefix = charBefore === "@" ? "" : "@";
162
+ const toInsert = prefix + cleanPath + " ";
163
+ const newValue = value.slice(0, cursorOffset) + toInsert + value.slice(cursorOffset);
164
+ updateValue(newValue, cursorOffset + toInsert.length);
165
+ return;
166
+ }
167
+ }
168
+ // Toggle Help
169
+ if (input === "?" && value.length === 0 && onHelpToggle) {
170
+ onHelpToggle();
171
+ return;
172
+ }
173
+ // Handle Autocomplete Navigation (only without shift modifier)
174
+ // Let Shift+Up/Down pass through for scrolling
175
+ if (showSuggestions && !key.shift) {
176
+ if (key.name === 'up') {
177
+ setActiveSuggestion((prev) => prev > 0 ? prev - 1 : suggestions.length - 1);
178
+ return;
179
+ }
180
+ if (key.name === 'down') {
181
+ setActiveSuggestion((prev) => prev < suggestions.length - 1 ? prev + 1 : 0);
182
+ return;
183
+ }
184
+ if (key.name === 'tab' || key.name === 'return') {
185
+ completeSuggestion(key.name === 'return');
186
+ return;
187
+ }
188
+ if (key.name === 'escape') {
189
+ setShowSuggestions(false);
190
+ return;
191
+ }
192
+ }
193
+ // Standard Input Handling
194
+ if (key.name === 'return') {
195
+ if (key.shift) {
196
+ // Shift+Enter: Add newline
197
+ const newValue = value.slice(0, cursorOffset) + "\n" + value.slice(cursorOffset);
198
+ updateValue(newValue, cursorOffset + 1);
199
+ }
200
+ else {
201
+ // Enter: Submit
202
+ if (value.trim()) {
203
+ onSubmit(value.trim());
204
+ setValue("");
205
+ setCursorOffset(0);
206
+ setShowSuggestions(false);
207
+ onInputChange?.("");
208
+ }
209
+ }
210
+ }
211
+ else if (key.name === 'backspace' || key.name === 'delete') {
212
+ if (cursorOffset > 0) {
213
+ const newValue = value.slice(0, cursorOffset - 1) + value.slice(cursorOffset);
214
+ updateValue(newValue, cursorOffset - 1);
215
+ }
216
+ }
217
+ else if (key.name === 'left') {
218
+ setCursorOffset(Math.max(0, cursorOffset - 1));
219
+ }
220
+ else if (key.name === 'right') {
221
+ setCursorOffset(Math.min(value.length, cursorOffset + 1));
222
+ }
223
+ else if (key.ctrl && input === "u") {
224
+ updateValue("", 0);
225
+ }
226
+ else if (key.name === 'tab' && !showSuggestions) {
227
+ // Ignore tab if no suggestions (prevents focus loss if mapped)
228
+ }
229
+ else if (key.paste) {
230
+ // Handle paste events (Cmd+V, bracketed paste)
231
+ const newValue = value.slice(0, cursorOffset) + input + value.slice(cursorOffset);
232
+ updateValue(newValue, cursorOffset + input.length);
233
+ }
234
+ else if (key.insertable && input) {
235
+ // Only insert if the key is actually insertable (not arrow keys, etc.)
236
+ const newValue = value.slice(0, cursorOffset) + input + value.slice(cursorOffset);
237
+ updateValue(newValue, cursorOffset + input.length);
238
+ }
239
+ }, { isActive: !disabled });
240
+ // Split into lines for display and calculate cursor position
241
+ const lines = value ? value.split("\n") : [];
242
+ const hasContent = value.trim().length > 0;
243
+ // Calculate which line and column the cursor should be on
244
+ let cursorLine = 0;
245
+ let cursorCol = cursorOffset;
246
+ let charCount = 0;
247
+ for (let i = 0; i < lines.length; i++) {
248
+ const lineLength = lines[i].length;
249
+ if (charCount + lineLength >= cursorOffset) {
250
+ cursorLine = i;
251
+ cursorCol = cursorOffset - charCount;
252
+ break;
253
+ }
254
+ charCount += lineLength + 1; // +1 for newline character
255
+ }
256
+ return (React.createElement(Box, { flexDirection: "column", width: "100%" },
257
+ showSuggestions && (React.createElement(Box, { borderColor: theme.border.default, borderStyle: "round", flexDirection: "column", marginBottom: 0, paddingX: 1 }, suggestions.map((file, idx) => (React.createElement(Text, { color: idx === activeSuggestion ? theme.text.accent : theme.text.dim, key: file },
258
+ idx === activeSuggestion ? "❯ " : " ",
259
+ " ",
260
+ file))))),
261
+ React.createElement(Box, { borderColor: disabled ? theme.border.default : theme.border.accent, borderStyle: "round", flexDirection: "column", marginBottom: 0, minHeight: 3, paddingX: 1, width: "100%" },
262
+ React.createElement(Box, { flexDirection: "row" },
263
+ React.createElement(Text, { color: theme.text.accent }, "\u276F "),
264
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
265
+ !hasContent && !disabled && (React.createElement(Text, null,
266
+ chalk.inverse(placeholder.slice(0, 1)),
267
+ React.createElement(Text, { color: theme.text.dim, italic: true }, placeholder.slice(1)))),
268
+ lines.length > 0 && (React.createElement(Box, { flexDirection: "column" }, lines.map((line, idx) => {
269
+ // Insert cursor at the correct position
270
+ if (idx === cursorLine && !disabled) {
271
+ const before = line.slice(0, cursorCol);
272
+ const charAtCursor = line[cursorCol] || ' ';
273
+ const after = line.slice(cursorCol + 1);
274
+ return (React.createElement(Text, { color: theme.text.primary, key: idx },
275
+ before,
276
+ chalk.inverse(charAtCursor),
277
+ after));
278
+ }
279
+ return (React.createElement(Text, { color: theme.text.primary, key: idx }, line));
280
+ }))),
281
+ !hasContent && disabled && (React.createElement(Text, { color: theme.text.dim, italic: true }, "Waiting for agent to complete..."))))),
282
+ React.createElement(Box, { paddingX: 1, justifyContent: "space-between" },
283
+ React.createElement(Box, null,
284
+ React.createElement(Text, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "⏸ plan mode on" : "▶ build mode"),
285
+ React.createElement(Text, { color: theme.text.dim }, " (shift+tab to cycle)")),
286
+ React.createElement(Text, { color: theme.text.dim }, stats.totalTokens >= 1000000
287
+ ? `${(stats.totalTokens / 1000000).toFixed(1)}M tokens`
288
+ : stats.totalTokens >= 1000
289
+ ? `${(stats.totalTokens / 1000).toFixed(1)}k tokens`
290
+ : `${stats.totalTokens} tokens`))));
291
+ });
292
+ InputPrompt.displayName = "InputPrompt";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Message List Component
3
+ * Displays all messages in the conversation
4
+ */
5
+ import { Box } from "ink";
6
+ import React from "react";
7
+ import { useSession } from "../contexts/SessionContext.js";
8
+ import { Header } from "./Header.js";
9
+ import { AssistantMessage } from "./messages/AssistantMessage.js";
10
+ import { ErrorMessage } from "./messages/ErrorMessage.js";
11
+ import { LoadingMessage } from "./messages/LoadingMessage.js";
12
+ import { ThinkingMessage } from "./messages/ThinkingMessage.js";
13
+ import { TodoMessage } from "./messages/TodoMessage.js";
14
+ import { ToolMessage } from "./messages/ToolMessage.js";
15
+ import { UserMessage } from "./messages/UserMessage.js";
16
+ import { Scrollable } from "./Scrollable.js";
17
+ export const MessageList = ({ terminalWidth, currentFolder, gitBranch }) => {
18
+ const { messages, updateMessageById, isAgentRunning } = useSession();
19
+ const renderMessage = (message) => {
20
+ switch (message.type) {
21
+ case "user":
22
+ return React.createElement(UserMessage, { key: message.id, text: message.content });
23
+ case "assistant":
24
+ return (React.createElement(AssistantMessage, { isPending: message.isPending, key: message.id, terminalWidth: terminalWidth, text: message.content }));
25
+ case "tool":
26
+ return (React.createElement(ToolMessage, { description: message.content, id: message.id, input: message.toolInput, isExpanded: message.isExpanded, key: message.id, onToggle: (id) => updateMessageById(id, { isExpanded: !message.isExpanded }), result: message.toolResult, toolName: message.toolName || "Unknown" }));
27
+ case "thinking":
28
+ return (React.createElement(ThinkingMessage, { content: message.content, id: message.id, isExpanded: message.isExpanded, key: message.id, onToggle: (id) => updateMessageById(id, { isExpanded: !message.isExpanded }) }));
29
+ case "error":
30
+ return (React.createElement(ErrorMessage, { key: message.id, message: message.content, type: message.errorType || "error" }));
31
+ case "todo":
32
+ return React.createElement(TodoMessage, { key: message.id, todos: message.todos || [] });
33
+ default:
34
+ return null;
35
+ }
36
+ };
37
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden" },
38
+ React.createElement(Scrollable, { scrollToBottom: true },
39
+ React.createElement(Header, { currentFolder: currentFolder, gitBranch: gitBranch, key: "header", version: "0.0.2" }),
40
+ messages.map((message) => (React.createElement(Box, { flexDirection: "column", key: message.id, width: "100%" }, renderMessage(message)))),
41
+ isAgentRunning && !messages.some(m => m.type === "assistant" && m.isPending) && (React.createElement(LoadingMessage, { key: "loading" })))));
42
+ };
@@ -0,0 +1,31 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { theme } from "../utils/theme.js";
4
+ const MAX_DISPLAYED_QUEUED_MESSAGES = 3;
5
+ export const QueuedMessageDisplay = ({ messageQueue, }) => {
6
+ if (messageQueue.length === 0) {
7
+ return null;
8
+ }
9
+ return (React.createElement(Box, { borderColor: theme.border.default, borderStyle: "round", flexDirection: "column", marginBottom: 1, marginTop: 0, paddingX: 1 },
10
+ React.createElement(Box, { marginBottom: 0 },
11
+ React.createElement(Text, { bold: true, color: theme.text.secondary },
12
+ "Queued (",
13
+ messageQueue.length,
14
+ ")")),
15
+ messageQueue
16
+ .slice(0, MAX_DISPLAYED_QUEUED_MESSAGES)
17
+ .map((message, index) => {
18
+ const preview = message.replace(/\s+/g, " ");
19
+ return (React.createElement(Box, { key: index, width: "100%" },
20
+ React.createElement(Text, { color: theme.text.dim },
21
+ " ",
22
+ index + 1,
23
+ ". "),
24
+ React.createElement(Text, { color: theme.text.primary, wrap: "truncate" }, preview)));
25
+ }),
26
+ messageQueue.length > MAX_DISPLAYED_QUEUED_MESSAGES && (React.createElement(Box, null,
27
+ React.createElement(Text, { color: theme.text.dim, italic: true },
28
+ "... (+",
29
+ messageQueue.length - MAX_DISPLAYED_QUEUED_MESSAGES,
30
+ " more)")))));
31
+ };