@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0

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 (200) hide show
  1. package/.oxlintrc.json +8 -0
  2. package/dist/index.mjs +12603 -0
  3. package/package.json +7 -39
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +9 -0
  6. package/src/__e2e__/README.md +196 -0
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
  10. package/src/__e2e__/conversation.e2e.test.tsx +56 -0
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
  12. package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
  13. package/src/__e2e__/helpers/test-helpers.ts +450 -0
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
  15. package/src/__e2e__/llm-models.e2e.test.ts +402 -0
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
  19. package/src/__e2e__/repl.e2e.test.tsx +78 -0
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
  23. package/src/args.ts +22 -0
  24. package/src/components/__tests__/react-compiler.test.tsx +78 -0
  25. package/src/components/__tests__/status-indicator.test.tsx +403 -0
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
  27. package/src/components/composer/agent-mode-indicator.tsx +63 -0
  28. package/src/components/composer/bash-runner.tsx +54 -0
  29. package/src/components/composer/commands/default-commands.tsx +615 -0
  30. package/src/components/composer/commands/handler.tsx +59 -0
  31. package/src/components/composer/commands/picker.tsx +273 -0
  32. package/src/components/composer/commands/registry.ts +233 -0
  33. package/src/components/composer/commands/types.ts +33 -0
  34. package/src/components/composer/context.tsx +88 -0
  35. package/src/components/composer/file-mention-picker.tsx +83 -0
  36. package/src/components/composer/help.tsx +44 -0
  37. package/src/components/composer/index.tsx +1006 -0
  38. package/src/components/composer/mentions.ts +57 -0
  39. package/src/components/composer/message-queue.tsx +70 -0
  40. package/src/components/composer/mode-panel.tsx +35 -0
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
  43. package/src/components/composer/modes/bash-handler.tsx +132 -0
  44. package/src/components/composer/modes/bash-renderer.tsx +175 -0
  45. package/src/components/composer/modes/default-handlers.tsx +33 -0
  46. package/src/components/composer/modes/index.ts +41 -0
  47. package/src/components/composer/modes/types.ts +21 -0
  48. package/src/components/composer/persistent-shell.ts +283 -0
  49. package/src/components/composer/process.ts +65 -0
  50. package/src/components/composer/types.ts +9 -0
  51. package/src/components/composer/use-mention-search.ts +68 -0
  52. package/src/components/error-boundry.tsx +60 -0
  53. package/src/components/exit-message.tsx +29 -0
  54. package/src/components/expanded-view.tsx +74 -0
  55. package/src/components/file-completion.tsx +127 -0
  56. package/src/components/header.tsx +47 -0
  57. package/src/components/logo.tsx +37 -0
  58. package/src/components/segments.tsx +356 -0
  59. package/src/components/status-indicator.tsx +306 -0
  60. package/src/components/tool-group-summary.tsx +263 -0
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
  62. package/src/components/tool-permissions/diff-preview.tsx +355 -0
  63. package/src/components/tool-permissions/index.ts +5 -0
  64. package/src/components/tool-permissions/permission-options.tsx +375 -0
  65. package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
  67. package/src/components/tools/agent/ask-user-question.tsx +101 -0
  68. package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
  69. package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
  70. package/src/components/tools/agent/handoff-to-main.tsx +27 -0
  71. package/src/components/tools/agent/subagent.tsx +37 -0
  72. package/src/components/tools/agent/todo-write.tsx +104 -0
  73. package/src/components/tools/browser/close-tab.tsx +58 -0
  74. package/src/components/tools/browser/computer.tsx +70 -0
  75. package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
  76. package/src/components/tools/browser/get-tab-content.tsx +51 -0
  77. package/src/components/tools/browser/navigate-to.tsx +59 -0
  78. package/src/components/tools/browser/new-tab.tsx +60 -0
  79. package/src/components/tools/browser/perform-action.tsx +63 -0
  80. package/src/components/tools/browser/refresh-tab.tsx +43 -0
  81. package/src/components/tools/browser/switch-tab.tsx +58 -0
  82. package/src/components/tools/filesystem/delete-file.tsx +104 -0
  83. package/src/components/tools/filesystem/edit.tsx +220 -0
  84. package/src/components/tools/filesystem/list-dir.tsx +78 -0
  85. package/src/components/tools/filesystem/read-file.tsx +180 -0
  86. package/src/components/tools/filesystem/upload-image.tsx +76 -0
  87. package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
  88. package/src/components/tools/index.ts +91 -0
  89. package/src/components/tools/mcp/mcp-tool.tsx +158 -0
  90. package/src/components/tools/search/fetch-url.tsx +73 -0
  91. package/src/components/tools/search/file-search.tsx +78 -0
  92. package/src/components/tools/search/grep.tsx +90 -0
  93. package/src/components/tools/search/semantic-search.tsx +66 -0
  94. package/src/components/tools/search/web-search.tsx +71 -0
  95. package/src/components/tools/shared/index.tsx +48 -0
  96. package/src/components/tools/shared/zod-coercion.ts +35 -0
  97. package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
  98. package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
  99. package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
  100. package/src/components/tools/types.ts +16 -0
  101. package/src/components/tools.tsx +66 -0
  102. package/src/components/ui/__tests__/divider.test.tsx +61 -0
  103. package/src/components/ui/__tests__/gradient.test.tsx +125 -0
  104. package/src/components/ui/__tests__/input.test.tsx +166 -0
  105. package/src/components/ui/__tests__/select.test.tsx +273 -0
  106. package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
  107. package/src/components/ui/blinking-indicator.tsx +25 -0
  108. package/src/components/ui/divider.tsx +162 -0
  109. package/src/components/ui/gradient.tsx +56 -0
  110. package/src/components/ui/input.tsx +228 -0
  111. package/src/components/ui/select.tsx +151 -0
  112. package/src/components/ui/shimmer.tsx +84 -0
  113. package/src/context/agent-mode.tsx +95 -0
  114. package/src/context/extension-file.tsx +136 -0
  115. package/src/context/network-activity.tsx +45 -0
  116. package/src/context/notification.tsx +62 -0
  117. package/src/context/shell-size.tsx +49 -0
  118. package/src/context/shell-title.tsx +38 -0
  119. package/src/entrypoints/print-mode.ts +312 -0
  120. package/src/entrypoints/repl.tsx +401 -0
  121. package/src/hooks/use-agent.ts +15 -0
  122. package/src/hooks/use-api-client.ts +1 -0
  123. package/src/hooks/use-available-height.ts +8 -0
  124. package/src/hooks/use-cleanup.ts +29 -0
  125. package/src/hooks/use-interrupt-manager.ts +242 -0
  126. package/src/hooks/use-models.ts +22 -0
  127. package/src/index.ts +217 -0
  128. package/src/lib/__tests__/ansi.test.ts +255 -0
  129. package/src/lib/__tests__/cli.test.ts +122 -0
  130. package/src/lib/__tests__/commands.test.ts +325 -0
  131. package/src/lib/__tests__/constants.test.ts +15 -0
  132. package/src/lib/__tests__/focusables.test.ts +25 -0
  133. package/src/lib/__tests__/fs.test.ts +231 -0
  134. package/src/lib/__tests__/markdown.test.tsx +348 -0
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
  136. package/src/lib/__tests__/mcpManagement.test.ts +38 -0
  137. package/src/lib/__tests__/path-paste.test.ts +144 -0
  138. package/src/lib/__tests__/path.test.ts +300 -0
  139. package/src/lib/__tests__/queries.test.ts +39 -0
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
  141. package/src/lib/__tests__/text-buffer.test.ts +328 -0
  142. package/src/lib/__tests__/text-utils.test.ts +32 -0
  143. package/src/lib/__tests__/timing.test.ts +78 -0
  144. package/src/lib/__tests__/utils.test.ts +238 -0
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
  146. package/src/lib/ansi.ts +150 -0
  147. package/src/lib/cli-push-server.ts +112 -0
  148. package/src/lib/cli.ts +44 -0
  149. package/src/lib/clipboard.ts +226 -0
  150. package/src/lib/command-utils.ts +93 -0
  151. package/src/lib/commands.ts +270 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/extension-connection.ts +181 -0
  154. package/src/lib/focusables.ts +7 -0
  155. package/src/lib/fs.ts +533 -0
  156. package/src/lib/markdown/code-block.tsx +63 -0
  157. package/src/lib/markdown/index.ts +4 -0
  158. package/src/lib/markdown/link.tsx +19 -0
  159. package/src/lib/markdown/markdown.tsx +372 -0
  160. package/src/lib/markdown/types.ts +15 -0
  161. package/src/lib/mcpCommandHandler.ts +121 -0
  162. package/src/lib/mcpManagement.ts +44 -0
  163. package/src/lib/path-paste.ts +185 -0
  164. package/src/lib/path.ts +179 -0
  165. package/src/lib/queries.ts +15 -0
  166. package/src/lib/standaloneMcpService.ts +688 -0
  167. package/src/lib/status-utils.ts +237 -0
  168. package/src/lib/test-utils.tsx +72 -0
  169. package/src/lib/text-buffer.ts +2415 -0
  170. package/src/lib/text-utils.ts +272 -0
  171. package/src/lib/timing.ts +63 -0
  172. package/src/lib/types.ts +295 -0
  173. package/src/lib/utils.ts +182 -0
  174. package/src/lib/vim-buffer-actions.ts +732 -0
  175. package/src/providers/agent.tsx +1075 -0
  176. package/src/providers/api-client.tsx +43 -0
  177. package/src/services/logger.ts +85 -0
  178. package/src/terminal/detection.ts +187 -0
  179. package/src/terminal/exit.ts +279 -0
  180. package/src/terminal/notification.ts +83 -0
  181. package/src/terminal/progress.ts +201 -0
  182. package/src/terminal/setup.ts +797 -0
  183. package/src/terminal/suspend.ts +58 -0
  184. package/src/terminal/types.ts +51 -0
  185. package/src/theme/context.tsx +57 -0
  186. package/src/theme/index.ts +4 -0
  187. package/src/theme/themed.tsx +35 -0
  188. package/src/theme/themes.json +546 -0
  189. package/src/theme/types.ts +110 -0
  190. package/src/tools/types.ts +59 -0
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
  192. package/src/tools/utils/tool-ui-components.tsx +631 -0
  193. package/src/tools/utils/zod-coercion.ts +35 -0
  194. package/tsconfig.json +11 -0
  195. package/tsconfig.node.json +29 -0
  196. package/tsconfig.test.json +27 -0
  197. package/tsdown.config.ts +17 -0
  198. package/vitest.config.ts +76 -0
  199. package/README.md +0 -28
  200. package/dist/index.js +0 -26
@@ -0,0 +1,95 @@
1
+ import React, { createContext, use, useEffect, useMemo, useReducer } from "react";
2
+
3
+ import { AgentMode } from "../components/composer/types.js";
4
+
5
+ interface AgentModeContextValue {
6
+ agentMode: AgentMode;
7
+ setAgentMode: (mode: AgentMode) => void;
8
+ cycleAgentMode: () => void;
9
+ isWaitingForYoloConfirm: boolean;
10
+ resetToNormal: () => void;
11
+ }
12
+
13
+ const AgentModeContext = createContext<AgentModeContextValue | null>(null);
14
+
15
+ export const useAgentMode = () => {
16
+ const ctx = use(AgentModeContext);
17
+ if (!ctx) {
18
+ throw new Error(`${useAgentMode.name} must be used within ${AgentModeProvider.name}`);
19
+ }
20
+ return ctx;
21
+ };
22
+
23
+ const YOLO_CONFIRM_DELAY = 3_000;
24
+
25
+ const MODE_CYCLE_ORDER: AgentMode[] = [AgentMode.Normal, AgentMode.AcceptEdits, AgentMode.PlanMode];
26
+
27
+ type State = { agentMode: AgentMode; isWaitingForYoloConfirm: boolean };
28
+ type Action =
29
+ | { type: "CYCLE" }
30
+ | { type: "SET"; mode: AgentMode }
31
+ | { type: "RESET" }
32
+ | { type: "YOLO_TIMEOUT" };
33
+
34
+ function reducer(state: State, action: Action): State {
35
+ switch (action.type) {
36
+ case "CYCLE": {
37
+ if (state.isWaitingForYoloConfirm) {
38
+ // Second press while waiting → confirm YOLO
39
+ return { agentMode: AgentMode.Yolo, isWaitingForYoloConfirm: false };
40
+ }
41
+ if (state.agentMode === AgentMode.Yolo) {
42
+ // Cycling from YOLO goes back to Normal
43
+ return { agentMode: AgentMode.Normal, isWaitingForYoloConfirm: false };
44
+ }
45
+ const currentIndex = MODE_CYCLE_ORDER.indexOf(state.agentMode);
46
+ if (currentIndex === -1) {
47
+ return { agentMode: AgentMode.Normal, isWaitingForYoloConfirm: false };
48
+ }
49
+ if (currentIndex === MODE_CYCLE_ORDER.length - 1) {
50
+ // At end of cycle order → ask for YOLO confirmation
51
+ return { agentMode: state.agentMode, isWaitingForYoloConfirm: true };
52
+ }
53
+ return { agentMode: MODE_CYCLE_ORDER[currentIndex + 1]!, isWaitingForYoloConfirm: false };
54
+ }
55
+ case "SET": {
56
+ return { agentMode: action.mode, isWaitingForYoloConfirm: false };
57
+ }
58
+ case "RESET": {
59
+ return { agentMode: AgentMode.Normal, isWaitingForYoloConfirm: false };
60
+ }
61
+ case "YOLO_TIMEOUT": {
62
+ return { ...state, isWaitingForYoloConfirm: false };
63
+ }
64
+ }
65
+ }
66
+
67
+ interface AgentModeProviderProps extends React.PropsWithChildren {
68
+ initialMode?: AgentMode;
69
+ }
70
+
71
+ export function AgentModeProvider({ children, initialMode }: AgentModeProviderProps) {
72
+ const [{ agentMode, isWaitingForYoloConfirm }, dispatch] = useReducer(reducer, {
73
+ agentMode: initialMode ?? AgentMode.Normal,
74
+ isWaitingForYoloConfirm: false,
75
+ });
76
+
77
+ useEffect(() => {
78
+ if (!isWaitingForYoloConfirm) return;
79
+ const id = setTimeout(() => dispatch({ type: "YOLO_TIMEOUT" }), YOLO_CONFIRM_DELAY);
80
+ return () => clearTimeout(id);
81
+ }, [isWaitingForYoloConfirm]);
82
+
83
+ const value = useMemo<AgentModeContextValue>(
84
+ () => ({
85
+ agentMode,
86
+ setAgentMode: (mode) => dispatch({ type: "SET", mode }),
87
+ cycleAgentMode: () => dispatch({ type: "CYCLE" }),
88
+ isWaitingForYoloConfirm,
89
+ resetToNormal: () => dispatch({ type: "RESET" }),
90
+ }),
91
+ [agentMode, isWaitingForYoloConfirm],
92
+ );
93
+
94
+ return <AgentModeContext.Provider value={value}>{children}</AgentModeContext.Provider>;
95
+ }
@@ -0,0 +1,136 @@
1
+ import { createContext, use, useEffect, useRef, useState } from "react";
2
+
3
+ import type { EditorState } from "../lib/cli-push-server.js";
4
+
5
+ import { CliPushServer } from "../lib/cli-push-server.js";
6
+ import { ExtensionConnection } from "../lib/extension-connection.js";
7
+
8
+ interface ExtensionFileContextValue {
9
+ currentFile: string | undefined;
10
+ selectionStartLine: number | undefined;
11
+ selectionEndLine: number | undefined;
12
+ connection: ExtensionConnection | null;
13
+ pushServer: CliPushServer | null;
14
+ }
15
+
16
+ const ExtensionFileContext = createContext<ExtensionFileContextValue | null>(null);
17
+
18
+ export function useExtensionFile() {
19
+ const ctx = use(ExtensionFileContext);
20
+ if (!ctx) {
21
+ throw new Error("useExtensionFile must be used within ExtensionFileProvider");
22
+ }
23
+ return ctx;
24
+ }
25
+
26
+ export function ExtensionFileProvider({
27
+ children,
28
+ initialFile,
29
+ }: React.PropsWithChildren<{ initialFile?: string }>) {
30
+ const [currentFile, setCurrentFile] = useState<string | undefined>(initialFile);
31
+ const [selectionStartLine, setSelectionStartLine] = useState<number | undefined>(undefined);
32
+ const [selectionEndLine, setSelectionEndLine] = useState<number | undefined>(undefined);
33
+ const [connection, setConnection] = useState<ExtensionConnection | null>(null);
34
+ const [pushServer, setPushServer] = useState<CliPushServer | null>(null);
35
+ const connectionRef = useRef<ExtensionConnection | null>(null);
36
+ const pushServerRef = useRef<CliPushServer | null>(null);
37
+
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+
41
+ const connectToExtension = async () => {
42
+ // Start the CLI push server first so the extension can connect back to us
43
+ const ps = new CliPushServer();
44
+ try {
45
+ await ps.start();
46
+ } catch {
47
+ // Push server failed to start — continue without it
48
+ }
49
+
50
+ if (cancelled) {
51
+ ps.dispose();
52
+ return;
53
+ }
54
+
55
+ pushServerRef.current = ps;
56
+ setPushServer(ps);
57
+
58
+ // Wire push-based state updates
59
+ ps.onEditorChange((state: EditorState) => {
60
+ setCurrentFile(state.file);
61
+ setSelectionStartLine(state.startLine);
62
+ setSelectionEndLine(state.endLine);
63
+ });
64
+
65
+ // Retry for up to 10 seconds with exponential backoff (covers extension startup delay)
66
+ const delays = [500, 1000, 2000, 3000, 4000];
67
+ for (const delay of [0, ...delays]) {
68
+ if (cancelled) return;
69
+ if (delay > 0) {
70
+ await new Promise((r) => setTimeout(r, delay));
71
+ }
72
+ if (cancelled) return;
73
+
74
+ try {
75
+ const conn = new ExtensionConnection(ps);
76
+ const connected = await conn.connect();
77
+
78
+ if (connected) {
79
+ if (cancelled) {
80
+ conn.dispose();
81
+ return;
82
+ }
83
+
84
+ // Set initial state from push server (extension pushes state on register)
85
+ const state = ps.getEditorState();
86
+ setCurrentFile(state.file);
87
+ setSelectionStartLine(state.startLine);
88
+ setSelectionEndLine(state.endLine);
89
+
90
+ conn.onFileChange((file, startLine, endLine) => {
91
+ setCurrentFile(file);
92
+ setSelectionStartLine(startLine);
93
+ setSelectionEndLine(endLine);
94
+ });
95
+
96
+ connectionRef.current = conn;
97
+ setConnection(conn);
98
+ return; // Connected — stop retrying
99
+ }
100
+ } catch {
101
+ // ignore, retry
102
+ }
103
+ }
104
+ };
105
+
106
+ void connectToExtension();
107
+
108
+ return () => {
109
+ cancelled = true;
110
+ if (connectionRef.current) {
111
+ connectionRef.current.dispose();
112
+ connectionRef.current = null;
113
+ setConnection(null);
114
+ }
115
+ if (pushServerRef.current) {
116
+ pushServerRef.current.dispose();
117
+ pushServerRef.current = null;
118
+ setPushServer(null);
119
+ }
120
+ };
121
+ }, []);
122
+
123
+ return (
124
+ <ExtensionFileContext.Provider
125
+ value={{
126
+ currentFile,
127
+ selectionStartLine,
128
+ selectionEndLine,
129
+ connection,
130
+ pushServer,
131
+ }}
132
+ >
133
+ {children}
134
+ </ExtensionFileContext.Provider>
135
+ );
136
+ }
@@ -0,0 +1,45 @@
1
+ import { createContext, use, useMemo, useState, type ReactNode } from "react";
2
+
3
+ export type NetworkActivity = "upload" | "download" | "idle";
4
+
5
+ interface NetworkActivityContextValue {
6
+ activity: NetworkActivity;
7
+ setActivity: (activity: NetworkActivity) => void;
8
+ startUpload: () => void;
9
+ startDownload: () => void;
10
+ stopActivity: () => void;
11
+ }
12
+
13
+ const NetworkActivityContext = createContext<NetworkActivityContextValue | null>(null);
14
+
15
+ export function NetworkActivityProvider({ children }: { children: ReactNode }) {
16
+ const [activity, setActivity] = useState<NetworkActivity>("idle");
17
+
18
+ const startUpload = () => setActivity("upload");
19
+ const startDownload = () => setActivity("download");
20
+ const stopActivity = () => setActivity("idle");
21
+
22
+ const value = useMemo(
23
+ () => ({
24
+ activity,
25
+ setActivity,
26
+ startUpload,
27
+ startDownload,
28
+ stopActivity,
29
+ }),
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ [activity],
32
+ );
33
+
34
+ return (
35
+ <NetworkActivityContext.Provider value={value}>{children}</NetworkActivityContext.Provider>
36
+ );
37
+ }
38
+
39
+ export function useNetworkActivity() {
40
+ const context = use(NetworkActivityContext);
41
+ if (!context) {
42
+ throw new Error("useNetworkActivity must be used within NetworkActivityProvider");
43
+ }
44
+ return context;
45
+ }
@@ -0,0 +1,62 @@
1
+ import { useFocusState } from "@codellm/jar";
2
+ import { createContext, useCallback, use, useMemo } from "react";
3
+
4
+ import { notify, notifyAction, notifyComplete } from "../terminal/notification.js";
5
+
6
+ interface NotificationContextValue {
7
+ notifyIfUnfocused: (message: string, title?: string) => Promise<void>;
8
+ notifyActionRequired: (message: string) => Promise<void>;
9
+ notifyTaskComplete: (message: string) => Promise<void>;
10
+ }
11
+
12
+ const NotificationContext = createContext<NotificationContextValue | null>(null);
13
+
14
+ export const useNotification = () => {
15
+ const context = use(NotificationContext);
16
+ if (!context) {
17
+ throw new Error("useNotification must be used within NotificationProvider");
18
+ }
19
+ return context;
20
+ };
21
+
22
+ export const NotificationProvider = ({ children }: React.PropsWithChildren) => {
23
+ const { isFocused } = useFocusState();
24
+
25
+ const notifyIfUnfocused = useCallback(
26
+ async (message: string, title?: string) => {
27
+ if (!isFocused) {
28
+ await notify({ message, title });
29
+ }
30
+ },
31
+ [isFocused],
32
+ );
33
+
34
+ const notifyActionRequired = useCallback(
35
+ async (message: string) => {
36
+ if (!isFocused) {
37
+ await notifyAction(message);
38
+ }
39
+ },
40
+ [isFocused],
41
+ );
42
+
43
+ const notifyTaskComplete = useCallback(
44
+ async (message: string) => {
45
+ if (!isFocused) {
46
+ await notifyComplete(message);
47
+ }
48
+ },
49
+ [isFocused],
50
+ );
51
+
52
+ const value = useMemo(
53
+ () => ({
54
+ notifyIfUnfocused,
55
+ notifyActionRequired,
56
+ notifyTaskComplete,
57
+ }),
58
+ [notifyIfUnfocused, notifyActionRequired, notifyTaskComplete],
59
+ );
60
+
61
+ return <NotificationContext.Provider value={value}>{children}</NotificationContext.Provider>;
62
+ };
@@ -0,0 +1,49 @@
1
+ import { createContext, use, useCallback, useLayoutEffect, useMemo, useState } from "react";
2
+
3
+ const SHELL_PADDING_X = 8;
4
+
5
+ interface ShellSizeContextValue {
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ export const ShellSizeContext = createContext<ShellSizeContextValue | null>(null);
11
+
12
+ export const useShellSize = () => {
13
+ const ctx = use(ShellSizeContext);
14
+ if (!ctx) {
15
+ throw new Error(`${useShellSize.name} must be used within a ${ShellSizeProvider.name}`);
16
+ }
17
+ return ctx;
18
+ };
19
+
20
+ export function ShellSizeProvider({ children }: React.PropsWithChildren) {
21
+ const [size, setSize] = useState({
22
+ columns: process.stdout.columns - SHELL_PADDING_X,
23
+ rows: process.stdout.rows,
24
+ });
25
+
26
+ const handleResize = useCallback(() => {
27
+ setSize({
28
+ columns: process.stdout.columns - SHELL_PADDING_X,
29
+ rows: process.stdout.rows,
30
+ });
31
+ }, []);
32
+
33
+ useLayoutEffect(() => {
34
+ process.stdout.on("resize", handleResize);
35
+ return () => {
36
+ process.stdout.off("resize", handleResize);
37
+ };
38
+ }, [handleResize]);
39
+
40
+ const value = useMemo<ShellSizeContextValue>(
41
+ () => ({
42
+ width: size.columns,
43
+ height: size.rows,
44
+ }),
45
+ [size.columns, size.rows],
46
+ );
47
+
48
+ return <ShellSizeContext.Provider value={value}>{children}</ShellSizeContext.Provider>;
49
+ }
@@ -0,0 +1,38 @@
1
+ import { createContext, useCallback, use } from "react";
2
+
3
+ import { useCleanup } from "../hooks/use-cleanup.js";
4
+ import { ANSI } from "../lib/ansi.js";
5
+
6
+ interface ShellTitleContextValue {
7
+ setTitle: (title: string) => void;
8
+ }
9
+
10
+ const ShellTitleContext = createContext<ShellTitleContextValue | null>(null);
11
+
12
+ export const useShellTitle = () => {
13
+ const ctx = use(ShellTitleContext);
14
+ if (!ctx) {
15
+ throw new Error(`${useShellTitle.name} must be used within a ${ShellTitleProvider.name}`);
16
+ }
17
+ return ctx;
18
+ };
19
+
20
+ export function ShellTitleProvider({ children }: React.PropsWithChildren) {
21
+ const setTitle = useCallback((title: string) => {
22
+ // Remove C0 control characters (U+0000-U+001F) and DEL (U+007F)
23
+ const cleanedTitle = title
24
+ .split("")
25
+ .filter((ch) => {
26
+ const code = ch.codePointAt(0)!;
27
+ return !(code <= 0x1f || code === 0x7f);
28
+ })
29
+ .join("");
30
+ process.stdout.write(`${ANSI.SET_TITLE}${cleanedTitle}${ANSI.BEL}`);
31
+ }, []);
32
+
33
+ useCleanup(() => {
34
+ process.stdout.write(`${ANSI.SET_TITLE}${ANSI.BEL}`);
35
+ });
36
+
37
+ return <ShellTitleContext.Provider value={{ setTitle }}>{children}</ShellTitleContext.Provider>;
38
+ }