@abacus-ai/cli 2.0.0-canary.1 → 2.0.0-canary.11

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 (198) hide show
  1. package/README.md +25 -0
  2. package/dist/index.mjs +466 -438
  3. package/package.json +4 -1
  4. package/.oxlintrc.json +0 -8
  5. package/resources/abacus.ico +0 -0
  6. package/resources/entitlements.plist +0 -9
  7. package/src/__e2e__/README.md +0 -196
  8. package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
  9. package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
  10. package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
  11. package/src/__e2e__/conversation.e2e.test.tsx +0 -56
  12. package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
  13. package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
  14. package/src/__e2e__/helpers/test-helpers.ts +0 -449
  15. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
  16. package/src/__e2e__/llm-models.e2e.test.ts +0 -402
  17. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
  18. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
  19. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
  20. package/src/__e2e__/repl.e2e.test.tsx +0 -78
  21. package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
  22. package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
  23. package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
  24. package/src/args.ts +0 -22
  25. package/src/components/__tests__/react-compiler.test.tsx +0 -78
  26. package/src/components/__tests__/status-indicator.test.tsx +0 -403
  27. package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
  28. package/src/components/composer/agent-mode-indicator.tsx +0 -63
  29. package/src/components/composer/bash-runner.tsx +0 -54
  30. package/src/components/composer/commands/default-commands.tsx +0 -615
  31. package/src/components/composer/commands/handler.tsx +0 -59
  32. package/src/components/composer/commands/picker.tsx +0 -273
  33. package/src/components/composer/commands/registry.ts +0 -233
  34. package/src/components/composer/commands/types.ts +0 -33
  35. package/src/components/composer/context.tsx +0 -88
  36. package/src/components/composer/file-mention-picker.tsx +0 -83
  37. package/src/components/composer/help.tsx +0 -44
  38. package/src/components/composer/index.tsx +0 -1007
  39. package/src/components/composer/mentions.ts +0 -57
  40. package/src/components/composer/message-queue.tsx +0 -70
  41. package/src/components/composer/mode-panel.tsx +0 -35
  42. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
  43. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
  44. package/src/components/composer/modes/bash-handler.tsx +0 -132
  45. package/src/components/composer/modes/bash-renderer.tsx +0 -175
  46. package/src/components/composer/modes/default-handlers.tsx +0 -33
  47. package/src/components/composer/modes/index.ts +0 -41
  48. package/src/components/composer/modes/types.ts +0 -21
  49. package/src/components/composer/persistent-shell.ts +0 -283
  50. package/src/components/composer/process.ts +0 -65
  51. package/src/components/composer/types.ts +0 -9
  52. package/src/components/composer/use-mention-search.ts +0 -68
  53. package/src/components/error-boundry.tsx +0 -60
  54. package/src/components/exit-message.tsx +0 -29
  55. package/src/components/expanded-view.tsx +0 -74
  56. package/src/components/file-completion.tsx +0 -127
  57. package/src/components/header.tsx +0 -47
  58. package/src/components/logo.tsx +0 -37
  59. package/src/components/segments.tsx +0 -356
  60. package/src/components/status-indicator.tsx +0 -306
  61. package/src/components/tool-group-summary.tsx +0 -263
  62. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -319
  63. package/src/components/tool-permissions/diff-preview.tsx +0 -359
  64. package/src/components/tool-permissions/index.ts +0 -5
  65. package/src/components/tool-permissions/permission-options.tsx +0 -401
  66. package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
  67. package/src/components/tool-permissions/tool-permission-ui.tsx +0 -420
  68. package/src/components/tools/agent/ask-user-question.tsx +0 -107
  69. package/src/components/tools/agent/enter-plan-mode.tsx +0 -55
  70. package/src/components/tools/agent/exit-plan-mode.tsx +0 -83
  71. package/src/components/tools/agent/handoff-to-main.tsx +0 -27
  72. package/src/components/tools/agent/subagent.tsx +0 -37
  73. package/src/components/tools/agent/todo-write.tsx +0 -104
  74. package/src/components/tools/browser/close-tab.tsx +0 -58
  75. package/src/components/tools/browser/computer.tsx +0 -70
  76. package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
  77. package/src/components/tools/browser/get-tab-content.tsx +0 -51
  78. package/src/components/tools/browser/navigate-to.tsx +0 -59
  79. package/src/components/tools/browser/new-tab.tsx +0 -60
  80. package/src/components/tools/browser/perform-action.tsx +0 -63
  81. package/src/components/tools/browser/refresh-tab.tsx +0 -43
  82. package/src/components/tools/browser/switch-tab.tsx +0 -58
  83. package/src/components/tools/filesystem/delete-file.tsx +0 -104
  84. package/src/components/tools/filesystem/edit.tsx +0 -220
  85. package/src/components/tools/filesystem/list-dir.tsx +0 -78
  86. package/src/components/tools/filesystem/read-file.tsx +0 -180
  87. package/src/components/tools/filesystem/upload-image.tsx +0 -76
  88. package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
  89. package/src/components/tools/index.ts +0 -91
  90. package/src/components/tools/mcp/mcp-tool.tsx +0 -158
  91. package/src/components/tools/search/fetch-url.tsx +0 -73
  92. package/src/components/tools/search/file-search.tsx +0 -78
  93. package/src/components/tools/search/grep.tsx +0 -90
  94. package/src/components/tools/search/semantic-search.tsx +0 -66
  95. package/src/components/tools/search/web-search.tsx +0 -71
  96. package/src/components/tools/shared/index.tsx +0 -48
  97. package/src/components/tools/shared/zod-coercion.ts +0 -35
  98. package/src/components/tools/terminal/bash-tool-output.tsx +0 -188
  99. package/src/components/tools/terminal/get-terminal-output.tsx +0 -91
  100. package/src/components/tools/terminal/run-in-terminal.tsx +0 -131
  101. package/src/components/tools/types.ts +0 -16
  102. package/src/components/tools.tsx +0 -68
  103. package/src/components/ui/__tests__/divider.test.tsx +0 -61
  104. package/src/components/ui/__tests__/gradient.test.tsx +0 -125
  105. package/src/components/ui/__tests__/input.test.tsx +0 -166
  106. package/src/components/ui/__tests__/select.test.tsx +0 -273
  107. package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
  108. package/src/components/ui/blinking-indicator.tsx +0 -27
  109. package/src/components/ui/divider.tsx +0 -162
  110. package/src/components/ui/gradient.tsx +0 -56
  111. package/src/components/ui/input.tsx +0 -228
  112. package/src/components/ui/select.tsx +0 -151
  113. package/src/components/ui/shimmer.tsx +0 -76
  114. package/src/context/agent-mode.tsx +0 -95
  115. package/src/context/extension-file.tsx +0 -136
  116. package/src/context/network-activity.tsx +0 -45
  117. package/src/context/notification.tsx +0 -62
  118. package/src/context/shell-size.tsx +0 -49
  119. package/src/context/shell-title.tsx +0 -38
  120. package/src/entrypoints/print-mode.ts +0 -312
  121. package/src/entrypoints/repl.tsx +0 -389
  122. package/src/hooks/use-agent.ts +0 -15
  123. package/src/hooks/use-api-client.ts +0 -1
  124. package/src/hooks/use-available-height.ts +0 -8
  125. package/src/hooks/use-cleanup.ts +0 -29
  126. package/src/hooks/use-interrupt-manager.ts +0 -242
  127. package/src/hooks/use-models.ts +0 -22
  128. package/src/index.ts +0 -217
  129. package/src/lib/__tests__/ansi.test.ts +0 -255
  130. package/src/lib/__tests__/cli.test.ts +0 -122
  131. package/src/lib/__tests__/commands.test.ts +0 -325
  132. package/src/lib/__tests__/constants.test.ts +0 -15
  133. package/src/lib/__tests__/focusables.test.ts +0 -25
  134. package/src/lib/__tests__/fs.test.ts +0 -231
  135. package/src/lib/__tests__/markdown.test.tsx +0 -348
  136. package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
  137. package/src/lib/__tests__/mcpManagement.test.ts +0 -38
  138. package/src/lib/__tests__/path-paste.test.ts +0 -144
  139. package/src/lib/__tests__/path.test.ts +0 -300
  140. package/src/lib/__tests__/queries.test.ts +0 -39
  141. package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
  142. package/src/lib/__tests__/text-buffer.test.ts +0 -328
  143. package/src/lib/__tests__/text-utils.test.ts +0 -32
  144. package/src/lib/__tests__/timing.test.ts +0 -78
  145. package/src/lib/__tests__/utils.test.ts +0 -238
  146. package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
  147. package/src/lib/ansi.ts +0 -150
  148. package/src/lib/cli-push-server.ts +0 -112
  149. package/src/lib/cli.ts +0 -44
  150. package/src/lib/clipboard.ts +0 -226
  151. package/src/lib/command-utils.ts +0 -93
  152. package/src/lib/commands.ts +0 -270
  153. package/src/lib/constants.ts +0 -3
  154. package/src/lib/extension-connection.ts +0 -181
  155. package/src/lib/focusables.ts +0 -7
  156. package/src/lib/fs.ts +0 -533
  157. package/src/lib/markdown/code-block.tsx +0 -63
  158. package/src/lib/markdown/index.ts +0 -4
  159. package/src/lib/markdown/link.tsx +0 -19
  160. package/src/lib/markdown/markdown.tsx +0 -372
  161. package/src/lib/markdown/types.ts +0 -15
  162. package/src/lib/mcpCommandHandler.ts +0 -121
  163. package/src/lib/mcpManagement.ts +0 -44
  164. package/src/lib/path-paste.ts +0 -185
  165. package/src/lib/path.ts +0 -179
  166. package/src/lib/queries.ts +0 -15
  167. package/src/lib/standaloneMcpService.ts +0 -688
  168. package/src/lib/status-utils.ts +0 -237
  169. package/src/lib/test-utils.tsx +0 -72
  170. package/src/lib/text-buffer.ts +0 -2415
  171. package/src/lib/text-utils.ts +0 -272
  172. package/src/lib/timing.ts +0 -63
  173. package/src/lib/types.ts +0 -295
  174. package/src/lib/utils.ts +0 -182
  175. package/src/lib/vim-buffer-actions.ts +0 -732
  176. package/src/providers/agent.tsx +0 -1063
  177. package/src/providers/api-client.tsx +0 -43
  178. package/src/services/logger.ts +0 -85
  179. package/src/terminal/detection.ts +0 -187
  180. package/src/terminal/exit.ts +0 -279
  181. package/src/terminal/notification.ts +0 -83
  182. package/src/terminal/progress.ts +0 -201
  183. package/src/terminal/setup.ts +0 -797
  184. package/src/terminal/types.ts +0 -51
  185. package/src/theme/context.tsx +0 -57
  186. package/src/theme/index.ts +0 -4
  187. package/src/theme/themed.tsx +0 -35
  188. package/src/theme/themes.json +0 -546
  189. package/src/theme/types.ts +0 -110
  190. package/src/tools/types.ts +0 -59
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +0 -33
  192. package/src/tools/utils/tool-ui-components.tsx +0 -649
  193. package/src/tools/utils/zod-coercion.ts +0 -35
  194. package/tsconfig.json +0 -16
  195. package/tsconfig.node.json +0 -29
  196. package/tsconfig.test.json +0 -27
  197. package/tsdown.config.ts +0 -17
  198. package/vitest.config.ts +0 -76
@@ -1,132 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState, type ReactElement } from "react";
2
-
3
- import { BashHandlerState, BashRenderer, CommandHistoryItem } from "./bash-renderer.js";
4
- import { ModeRenderProps } from "./types.js";
5
-
6
- export type BashHandlerApi = {
7
- submitCommand: (command: string) => void;
8
- renderer: (props: ModeRenderProps) => ReactElement;
9
- navigateHistory: (direction: "up" | "down") => string | null;
10
- getActivity: () => "idle" | "pending" | "error";
11
- };
12
-
13
- // Maximum number of commands to keep in history to prevent unbounded growth
14
- const MAX_COMMAND_HISTORY = 1000;
15
- const MAX_INPUT_HISTORY = 1000;
16
-
17
- export function useBashHandler(): BashHandlerApi {
18
- const [state, setState] = useState<BashHandlerState>({
19
- submittedCommand: null,
20
- runId: 0,
21
- output: "",
22
- isRunning: false,
23
- });
24
-
25
- const [commandHistoryItems, setCommandHistoryItems] = useState<CommandHistoryItem[]>([]);
26
- const [inputHistory, setInputHistory] = useState<string[]>([]);
27
- const [historyIndex, setHistoryIndex] = useState<number>(-1);
28
- const shellHistoryLoaded = useRef(false);
29
-
30
- useEffect(() => {
31
- if (shellHistoryLoaded.current) return;
32
- shellHistoryLoaded.current = true;
33
- // @ts-expect-error — shell-history ships no type declarations
34
- import("shell-history")
35
- .then((mod: { shellHistory?: () => string[]; default?: () => string[] }) => {
36
- const fn = mod.shellHistory ?? mod.default;
37
- const history: string[] = typeof fn === "function" ? fn() : [];
38
- setInputHistory(history.slice(-MAX_INPUT_HISTORY));
39
- })
40
- .catch(() => {});
41
- }, []);
42
-
43
- const submitCommand: BashHandlerApi["submitCommand"] = useCallback((command: string) => {
44
- const trimmedCommand = command.trim();
45
- if (!trimmedCommand) return;
46
-
47
- // Add to input history for up/down navigation, dedup consecutive identical commands (like HISTCONTROL=ignoredups)
48
- setInputHistory((prev) => {
49
- if (prev.length > 0 && prev[prev.length - 1] === trimmedCommand) {
50
- return prev;
51
- }
52
- const updated = [...prev, trimmedCommand];
53
- return updated.slice(-MAX_INPUT_HISTORY);
54
- });
55
- setHistoryIndex(-1);
56
-
57
- // Incrementing runId will trigger BashRenderer's useEffect to cancel any previous command
58
- setState((prev) => ({
59
- submittedCommand: trimmedCommand,
60
- runId: prev.runId + 1,
61
- output: "",
62
- isRunning: true,
63
- }));
64
- }, []);
65
-
66
- const onCommandComplete = useCallback((output: string) => {
67
- // Move the completed command to history, keeping only the most recent MAX_COMMAND_HISTORY items
68
- setState((prev) => {
69
- if (prev.submittedCommand) {
70
- setCommandHistoryItems((history) => {
71
- const updated = [...history, { command: prev.submittedCommand!, output }];
72
- return updated.slice(-MAX_COMMAND_HISTORY);
73
- });
74
- }
75
-
76
- // Clear the current command and return to idle
77
- return {
78
- submittedCommand: null,
79
- runId: prev.runId,
80
- output: "",
81
- isRunning: false,
82
- };
83
- });
84
- }, []);
85
-
86
- const renderer: BashHandlerApi["renderer"] = useCallback(
87
- (props: ModeRenderProps) => {
88
- return (
89
- <BashRenderer
90
- state={state}
91
- setState={setState}
92
- onCommandComplete={onCommandComplete}
93
- commandHistory={commandHistoryItems}
94
- currentPrompt={props.prompt}
95
- availableHeight={props.availableHeight}
96
- />
97
- );
98
- },
99
- [state, onCommandComplete, commandHistoryItems],
100
- );
101
-
102
- const navigateHistory: BashHandlerApi["navigateHistory"] = useCallback(
103
- (direction: "up" | "down") => {
104
- if (inputHistory.length === 0) return null;
105
-
106
- let newIndex = historyIndex;
107
- if (direction === "up") {
108
- // Navigate to previous command, but don't go below index 0 (stays at first command)
109
- newIndex = historyIndex === -1 ? inputHistory.length - 1 : Math.max(0, historyIndex - 1);
110
- } else {
111
- newIndex = historyIndex === -1 ? -1 : historyIndex + 1;
112
- if (newIndex >= inputHistory.length) {
113
- newIndex = -1;
114
- }
115
- }
116
-
117
- setHistoryIndex(newIndex);
118
- return newIndex === -1 ? "" : inputHistory[newIndex];
119
- },
120
- [inputHistory, historyIndex],
121
- );
122
-
123
- return {
124
- submitCommand,
125
- renderer,
126
- navigateHistory,
127
- getActivity: (): "idle" | "pending" | "error" => {
128
- if (state.isRunning) return "pending";
129
- return "idle";
130
- },
131
- };
132
- }
@@ -1,175 +0,0 @@
1
- import { View } from "@codellm/jar";
2
- import { useEffect, useMemo, useRef } from "react";
3
-
4
- import { stripDangerousSequences } from "../../../lib/text-utils.js";
5
- import { BashRunner } from "../bash-runner.js";
6
- import { getPersistentShell } from "../persistent-shell.js";
7
-
8
- export type BashHandlerState = {
9
- submittedCommand: string | null;
10
- runId: number;
11
- output: string;
12
- isRunning: boolean;
13
- };
14
-
15
- export type CommandHistoryItem = {
16
- command: string;
17
- output: string;
18
- };
19
-
20
- export type BashRendererProps = {
21
- state: BashHandlerState;
22
- setState: React.Dispatch<React.SetStateAction<BashHandlerState>>;
23
- onCommandComplete: (output: string) => void;
24
- commandHistory: CommandHistoryItem[];
25
- currentPrompt: string;
26
- availableHeight: number;
27
- };
28
-
29
- export function BashRenderer({
30
- state,
31
- setState,
32
- onCommandComplete,
33
- commandHistory,
34
- currentPrompt,
35
- availableHeight,
36
- }: BashRendererProps) {
37
- const { submittedCommand, runId } = state;
38
- const normalized = useMemo(() => submittedCommand?.trim() || "", [submittedCommand]);
39
- const lastRunIdRef = useRef<number>(-1);
40
- // Use AbortController for better cancellation handling
41
- const abortControllerRef = useRef<AbortController | null>(null);
42
-
43
- useEffect(() => {
44
- if (!normalized) return;
45
- if (runId === lastRunIdRef.current) return;
46
- lastRunIdRef.current = runId;
47
-
48
- // Cancel any previous command execution
49
- if (abortControllerRef.current) {
50
- abortControllerRef.current.abort();
51
- }
52
-
53
- // Create new AbortController for this command
54
- const abortController = new AbortController();
55
- abortControllerRef.current = abortController;
56
-
57
- setState((prev) => ({ ...prev, isRunning: true, output: "" }));
58
- runBash({
59
- command: normalized,
60
- setOutput: (output: string) => {
61
- setState((prev) => ({ ...prev, output }));
62
- },
63
- signal: abortController.signal,
64
- })
65
- .then((output) => {
66
- // Only process result if not cancelled
67
- if (!abortController.signal.aborted) {
68
- setState((prev) => ({ ...prev, isRunning: false }));
69
- // Call onCommandComplete even when output is empty to signal completion
70
- // If output is undefined, it means the command was cancelled, so don't call onCommandComplete
71
- if (output !== undefined) {
72
- onCommandComplete(output);
73
- }
74
- }
75
- })
76
- .catch((_error) => {
77
- // Only process error if not cancelled
78
- if (!abortController.signal.aborted) {
79
- setState((prev) => ({ ...prev, isRunning: false }));
80
- // Pass empty string for errors (not undefined, to distinguish from cancellation)
81
- onCommandComplete("");
82
- }
83
- });
84
-
85
- return () => {
86
- // Cancel command execution on cleanup
87
- abortController.abort();
88
- abortControllerRef.current = null;
89
- };
90
- }, [normalized, runId, setState, onCommandComplete]);
91
-
92
- const isTyping = currentPrompt.trim().length > 0;
93
-
94
- const historyToShow = submittedCommand ? 2 : 3;
95
- const displayHistory = commandHistory.slice(-historyToShow);
96
- const historyStartIndex = Math.max(0, commandHistory.length - historyToShow);
97
-
98
- const totalCommands = displayHistory.length + (submittedCommand ? 1 : 0);
99
-
100
- const availableForOutput = Math.max(1, availableHeight - totalCommands);
101
-
102
- const maxLinesPerCommand =
103
- totalCommands > 0 ? Math.max(3, Math.floor(availableForOutput / totalCommands)) : 10;
104
-
105
- const shouldTruncate = isTyping || availableHeight < 15 || totalCommands > 1;
106
-
107
- return (
108
- <View flexDirection="column">
109
- {displayHistory.map((item, index) => {
110
- // Use the original index from the full commandHistory array to ensure uniqueness
111
- // This prevents key conflicts when the slice changes
112
- const originalIndex = historyStartIndex + index;
113
- return (
114
- <BashRunner
115
- key={`history-${originalIndex}`}
116
- output={item.output}
117
- command={item.command}
118
- truncate={shouldTruncate}
119
- maxLines={maxLinesPerCommand}
120
- />
121
- );
122
- })}
123
- {submittedCommand && (
124
- <BashRunner
125
- key={`submitted-${runId}`}
126
- output={state.output}
127
- command={normalized || submittedCommand}
128
- truncate={shouldTruncate}
129
- maxLines={maxLinesPerCommand}
130
- />
131
- )}
132
- </View>
133
- );
134
- }
135
-
136
- type RunProps = {
137
- command: string;
138
- setOutput: (output: string) => void;
139
- signal: AbortSignal;
140
- };
141
-
142
- async function runBash({ command, setOutput, signal }: RunProps): Promise<string | undefined> {
143
- const shell = getPersistentShell();
144
- const { stream, exit } = await shell.executeCommand(command);
145
-
146
- let commandOutput = "";
147
- try {
148
- for await (const chunk of stream) {
149
- // Check for cancellation at the start of each iteration
150
- if (signal.aborted) {
151
- return undefined;
152
- }
153
- // Strip dangerous ANSI sequences but preserve colors (claude.js pattern)
154
- commandOutput += stripDangerousSequences(chunk.data);
155
- // Only update output if not cancelled
156
- if (!signal.aborted) {
157
- setOutput(commandOutput);
158
- }
159
- }
160
-
161
- // Check cancellation before waiting for exit
162
- if (signal.aborted) {
163
- return undefined;
164
- }
165
-
166
- await exit;
167
- return commandOutput;
168
- } catch (error) {
169
- // If cancelled, return undefined; otherwise rethrow
170
- if (signal.aborted) {
171
- return undefined;
172
- }
173
- throw error;
174
- }
175
- }
@@ -1,33 +0,0 @@
1
- import { AgentModeIndicator } from "../agent-mode-indicator.js";
2
- import { FileMentionPicker } from "../file-mention-picker.js";
3
- import { Help } from "../help.js";
4
- import { ComposerMode } from "../types.js";
5
- import { ModeHandler } from "./types.js";
6
-
7
- export function createPromptHandler(): ModeHandler {
8
- return {
9
- mode: ComposerMode.Prompt,
10
- render: ({ prompt: _prompt, hints, availableHeight }) => {
11
- return <AgentModeIndicator hints={hints} availableHeight={availableHeight} />;
12
- },
13
- };
14
- }
15
-
16
- export function createHelpHandler(): ModeHandler {
17
- return {
18
- mode: ComposerMode.Help,
19
- render: ({ availableHeight }) => <Help availableHeight={availableHeight} />,
20
- };
21
- }
22
-
23
- export function createFilesHandler(onExit: () => void): ModeHandler {
24
- return {
25
- mode: ComposerMode.Files,
26
- render: ({ prompt: _prompt, availableHeight }) => (
27
- <FileMentionPicker onAccept={onExit} onCancel={onExit} availableHeight={availableHeight} />
28
- ),
29
- shouldPreventDefault: (_input, key) => {
30
- return key.upArrow || key.downArrow || key.escape || key.return || key.tab;
31
- },
32
- };
33
- }
@@ -1,41 +0,0 @@
1
- import { useMemo } from "react";
2
-
3
- import { useCommandHandler } from "../commands/handler.js";
4
- import { ComposerMode } from "../types.js";
5
- import { useBashHandler } from "./bash-handler.js";
6
- import { createPromptHandler, createHelpHandler, createFilesHandler } from "./default-handlers.js";
7
- import { ModeHandler } from "./types.js";
8
-
9
- export function useModeHandlers(onExitFilesMode: () => void, clearBuffer: () => void) {
10
- const bashHandler = useBashHandler();
11
- const commandHandler = useCommandHandler(clearBuffer);
12
-
13
- const handlers = useMemo(() => {
14
- const bashModeHandler: ModeHandler = {
15
- mode: ComposerMode.Bash,
16
- render: bashHandler.renderer,
17
- onSubmit: bashHandler.submitCommand,
18
- };
19
-
20
- const commandModeHandler: ModeHandler = {
21
- mode: ComposerMode.Command,
22
- render: commandHandler.renderer,
23
- onSubmit: commandHandler.executeCommand,
24
- shouldPreventDefault: commandHandler.shouldPreventDefault,
25
- };
26
-
27
- return new Map<ComposerMode, ModeHandler>([
28
- [ComposerMode.Prompt, createPromptHandler()],
29
- [ComposerMode.Help, createHelpHandler()],
30
- [ComposerMode.Bash, bashModeHandler],
31
- [ComposerMode.Command, commandModeHandler],
32
- [ComposerMode.Files, createFilesHandler(onExitFilesMode)],
33
- ]);
34
- }, [bashHandler, commandHandler, onExitFilesMode]);
35
-
36
- return {
37
- handlers,
38
- getActivity: bashHandler.getActivity,
39
- bashNavigateHistory: bashHandler.navigateHistory,
40
- };
41
- }
@@ -1,21 +0,0 @@
1
- import type { Key } from "@codellm/jar";
2
-
3
- import { ReactNode } from "react";
4
-
5
- import type { InterruptHints } from "../../../hooks/use-interrupt-manager.js";
6
-
7
- import { ComposerMode } from "../types.js";
8
-
9
- export interface ModeHandler {
10
- mode: ComposerMode;
11
- render: (props: ModeRenderProps) => ReactNode;
12
- onSubmit?: (value: string) => void;
13
- shouldPreventDefault?: (input: string, key: Key) => boolean;
14
- }
15
-
16
- export interface ModeRenderProps {
17
- prompt: string;
18
- onExit: () => void;
19
- hints?: InterruptHints;
20
- availableHeight: number;
21
- }
@@ -1,283 +0,0 @@
1
- import { spawn, ChildProcess } from "node:child_process";
2
- import * as os from "node:os";
3
-
4
- export type StreamChunk = { type: "stdout" | "stderr"; data: string };
5
-
6
- // Constants from claude.js patterns
7
- const GRACEFUL_SHUTDOWN_DELAY_MS = 5000;
8
-
9
- // Signals to forward to child processes (claude.js lines 3290-3294)
10
- const FORWARDED_SIGNALS: NodeJS.Signals[] = ["SIGUSR1", "SIGUSR2", "SIGTERM", "SIGINT", "SIGHUP"];
11
-
12
- function getShellCommand(): { command: string; args: string[]; env: Record<string, string> } {
13
- const platform = os.platform();
14
-
15
- // Filter out undefined values from process.env
16
- const baseEnv: Record<string, string> = {};
17
- for (const [key, value] of Object.entries(process.env)) {
18
- if (value !== undefined) {
19
- baseEnv[key] = value;
20
- }
21
- }
22
-
23
- if (platform === "win32") {
24
- // Windows: Use PowerShell or cmd
25
- return {
26
- command: process.env.COMSPEC || "cmd.exe",
27
- args: [],
28
- env: baseEnv,
29
- };
30
- } else {
31
- // Unix-like systems (macOS, Linux)
32
- const shell = process.env.SHELL || "/bin/bash";
33
- return {
34
- command: shell,
35
- args: [],
36
- env: {
37
- ...baseEnv,
38
- BASH_SILENCE_DEPRECATION_WARNING: "1", // Silence macOS zsh warning
39
- PS1: "", // Disable prompt for bash
40
- },
41
- };
42
- }
43
- }
44
-
45
- export class PersistentShell {
46
- private shell: ChildProcess | null = null;
47
- private readonly queue: StreamChunk[] = [];
48
- private notify: (() => void) | null = null;
49
- private isProcessing = false;
50
- private destroyed = false;
51
- private readonly isWindows: boolean;
52
- private readonly signalHandlers = new Map<NodeJS.Signals, (...args: any[]) => void>();
53
-
54
- constructor() {
55
- this.isWindows = os.platform() === "win32";
56
- this.startShell();
57
- this.setupSignalForwarding();
58
- }
59
-
60
- /**
61
- * Setup signal forwarding to shell process (claude.js pattern).
62
- * Forwards SIGUSR1, SIGUSR2, SIGTERM, SIGINT, SIGHUP to child.
63
- */
64
- private setupSignalForwarding(): void {
65
- for (const signal of FORWARDED_SIGNALS) {
66
- const handler = () => {
67
- if (this.shell && !this.shell.killed && this.shell.exitCode === null) {
68
- try {
69
- this.shell.kill(signal);
70
- } catch {
71
- // Ignore errors when forwarding signals
72
- }
73
- }
74
- };
75
- this.signalHandlers.set(signal, handler);
76
- process.on(signal, handler);
77
- }
78
- }
79
-
80
- /**
81
- * Remove signal handlers during cleanup.
82
- */
83
- private removeSignalHandlers(): void {
84
- for (const [signal, handler] of this.signalHandlers) {
85
- process.removeListener(signal, handler);
86
- }
87
- this.signalHandlers.clear();
88
- }
89
-
90
- private startShell(): void {
91
- if (this.shell || this.destroyed) {
92
- return;
93
- }
94
-
95
- const { command, args, env } = getShellCommand();
96
-
97
- // Start a persistent shell session
98
- this.shell = spawn(command, args, {
99
- stdio: ["pipe", "pipe", "pipe"],
100
- env,
101
- cwd: process.cwd(),
102
- shell: false, // Don't spawn through another shell
103
- });
104
-
105
- if (this.shell.stdin) {
106
- // Suppress EPIPE errors — these occur when the shell dies and we try to write
107
- this.shell.stdin.on("error", (err: NodeJS.ErrnoException) => {
108
- if (err.code !== "EPIPE") {
109
- throw err;
110
- }
111
- });
112
- }
113
-
114
- if (this.shell.stdout) {
115
- this.shell.stdout.on("data", (chunk: Buffer) => {
116
- const data = chunk.toString("utf8");
117
- this.queue.push({ type: "stdout", data });
118
- if (this.notify) {
119
- const n = this.notify;
120
- this.notify = null;
121
- n();
122
- }
123
- });
124
- }
125
-
126
- if (this.shell.stderr) {
127
- this.shell.stderr.on("data", (chunk: Buffer) => {
128
- const data = chunk.toString("utf8");
129
- // Filter out common shell warnings/deprecation messages
130
- const shouldIgnore =
131
- data.includes("no job control in this shell") ||
132
- data.includes("The default interactive shell is now zsh") ||
133
- data.includes("To update your account to use zsh") ||
134
- data.includes("please run `chsh -s /bin/zsh`") ||
135
- data.includes("For more details, please visit");
136
-
137
- if (!shouldIgnore) {
138
- this.queue.push({ type: "stderr", data });
139
- if (this.notify) {
140
- const n = this.notify;
141
- this.notify = null;
142
- n();
143
- }
144
- }
145
- });
146
- }
147
-
148
- this.shell.on("exit", () => {
149
- this.shell = null;
150
- // Restart shell if it exits
151
- this.startShell();
152
- });
153
- }
154
-
155
- public async executeCommand(command: string): Promise<{
156
- stream: AsyncGenerator<StreamChunk, void, void>;
157
- exit: Promise<number>;
158
- }> {
159
- if (!this.shell || !this.shell.stdin) {
160
- throw new Error("Shell not initialized");
161
- }
162
-
163
- // Clear any pending output from previous commands
164
- this.queue.length = 0;
165
-
166
- const uniqueMarker = `__CMD_END_${Date.now()}_${Math.random().toString(36).substring(7)}__`;
167
-
168
- // Platform-specific command formatting
169
- let commandWithMarker: string;
170
- if (this.isWindows) {
171
- // Windows cmd.exe or PowerShell
172
- commandWithMarker = `${command}\necho ${uniqueMarker}\n`;
173
- } else {
174
- // Unix-like systems (bash, zsh, etc.)
175
- commandWithMarker = `${command}\necho "${uniqueMarker}"\n`;
176
- }
177
-
178
- this.shell.stdin.write(commandWithMarker);
179
- this.isProcessing = true;
180
-
181
- const stream = this.createStream(uniqueMarker);
182
- const exit = this.createExitPromise(uniqueMarker);
183
-
184
- return { stream, exit };
185
- }
186
-
187
- private async *createStream(marker: string): AsyncGenerator<StreamChunk, void, void> {
188
- while (true) {
189
- while (this.queue.length > 0) {
190
- const item = this.queue.shift();
191
- if (!item) {
192
- continue;
193
- }
194
-
195
- // Check if this chunk contains the marker
196
- if (item.data.includes(marker)) {
197
- // Remove the marker from the output
198
- const cleanedData = item.data.replace(marker, "").replace(/\n$/, "");
199
- if (cleanedData) {
200
- yield { type: item.type, data: cleanedData };
201
- }
202
- this.isProcessing = false;
203
- return;
204
- }
205
-
206
- yield item;
207
- }
208
-
209
- if (!this.isProcessing) {
210
- return;
211
- }
212
-
213
- await new Promise<void>((resolve) => {
214
- this.notify = resolve;
215
- });
216
- }
217
- }
218
-
219
- private async createExitPromise(_marker: string): Promise<number> {
220
- // Wait for the marker to appear in the output
221
- while (this.isProcessing) {
222
- await new Promise<void>((resolve) => setTimeout(resolve, 100));
223
- }
224
- return 0; // Assume success for now
225
- }
226
-
227
- /**
228
- * Gracefully kill the shell process (claude.js pattern).
229
- * Sends SIGTERM first, then SIGKILL after timeout.
230
- */
231
- private killProcessGracefully(process: ChildProcess): void {
232
- try {
233
- process.kill("SIGTERM");
234
-
235
- const killTimeout = setTimeout(() => {
236
- if (process && !process.killed) {
237
- process.kill("SIGKILL");
238
- }
239
- }, GRACEFUL_SHUTDOWN_DELAY_MS);
240
-
241
- // Unref so timer doesn't keep process alive
242
- if (killTimeout.unref) {
243
- killTimeout.unref();
244
- }
245
- } catch {
246
- // Ignore errors when killing process
247
- }
248
- }
249
-
250
- public destroy(): void {
251
- this.destroyed = true;
252
- // Remove signal handlers first
253
- this.removeSignalHandlers();
254
-
255
- if (this.shell) {
256
- // Suppress EPIPE errors from stdin after the shell is killed
257
- this.shell.stdin?.on("error", (err: NodeJS.ErrnoException) => {
258
- if (err.code !== "EPIPE") {
259
- throw err;
260
- }
261
- });
262
- this.killProcessGracefully(this.shell);
263
- this.shell = null;
264
- }
265
- }
266
- }
267
-
268
- // Singleton instance
269
- let shellInstance: PersistentShell | null = null;
270
-
271
- export function getPersistentShell(): PersistentShell {
272
- if (!shellInstance) {
273
- shellInstance = new PersistentShell();
274
- }
275
- return shellInstance;
276
- }
277
-
278
- export function destroyPersistentShell(): void {
279
- if (shellInstance) {
280
- shellInstance.destroy();
281
- shellInstance = null;
282
- }
283
- }