otherwise-cli 0.1.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 (81) hide show
  1. package/README.md +193 -0
  2. package/bin/otherwise.js +5 -0
  3. package/frontend/404.html +84 -0
  4. package/frontend/assets/OpenDyslexic3-Bold-CDyRs55Y.ttf +0 -0
  5. package/frontend/assets/OpenDyslexic3-Regular-CIBXa4WE.ttf +0 -0
  6. package/frontend/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  7. package/frontend/assets/conversational-worker-CeKiciGk.js +2929 -0
  8. package/frontend/assets/dictation-worker-D0aYfq8b.js +29 -0
  9. package/frontend/assets/gemini-color-CgSQmmva.png +0 -0
  10. package/frontend/assets/index-BLux5ps4.js +21 -0
  11. package/frontend/assets/index-Blh8_TEM.js +5272 -0
  12. package/frontend/assets/index-BpQ1PuKu.js +18 -0
  13. package/frontend/assets/index-Df737c8w.css +1 -0
  14. package/frontend/assets/index-xaYHL6wb.js +113 -0
  15. package/frontend/assets/ort-wasm-simd-threaded.asyncify-BynIiDiv.wasm +0 -0
  16. package/frontend/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
  17. package/frontend/assets/transformers-tULNc5V3.js +31 -0
  18. package/frontend/assets/tts-worker-DPJWqT7N.js +2899 -0
  19. package/frontend/assets/voice-mode-worker-GzvIE_uh.js +2927 -0
  20. package/frontend/assets/worker-2d5ABSLU.js +31 -0
  21. package/frontend/banner.png +0 -0
  22. package/frontend/favicon.svg +3 -0
  23. package/frontend/google55e5ec47ee14a5f8.html +1 -0
  24. package/frontend/index.html +234 -0
  25. package/frontend/manifest.json +17 -0
  26. package/frontend/pdf.worker.min.mjs +21 -0
  27. package/frontend/robots.txt +5 -0
  28. package/frontend/sitemap.xml +27 -0
  29. package/package.json +81 -0
  30. package/src/agent/index.js +1066 -0
  31. package/src/agent/location.js +51 -0
  32. package/src/agent/prompt.js +548 -0
  33. package/src/agent/tools.js +4372 -0
  34. package/src/browser/detect.js +68 -0
  35. package/src/browser/session.js +1109 -0
  36. package/src/config.js +137 -0
  37. package/src/email/client.js +503 -0
  38. package/src/index.js +557 -0
  39. package/src/inference/anthropic.js +113 -0
  40. package/src/inference/google.js +373 -0
  41. package/src/inference/index.js +81 -0
  42. package/src/inference/ollama.js +383 -0
  43. package/src/inference/openai.js +140 -0
  44. package/src/inference/openrouter.js +378 -0
  45. package/src/inference/xai.js +200 -0
  46. package/src/logBridge.js +9 -0
  47. package/src/models.js +146 -0
  48. package/src/remote/client.js +225 -0
  49. package/src/scheduler/cron.js +243 -0
  50. package/src/server.js +3876 -0
  51. package/src/storage/db.js +1135 -0
  52. package/src/storage/supabase.js +364 -0
  53. package/src/tunnel/cloudflare.js +241 -0
  54. package/src/ui/components/App.jsx +687 -0
  55. package/src/ui/components/BrowserSelect.jsx +111 -0
  56. package/src/ui/components/FilePicker.jsx +472 -0
  57. package/src/ui/components/Header.jsx +444 -0
  58. package/src/ui/components/HelpPanel.jsx +173 -0
  59. package/src/ui/components/HistoryPanel.jsx +158 -0
  60. package/src/ui/components/MessageList.jsx +235 -0
  61. package/src/ui/components/ModelSelector.jsx +304 -0
  62. package/src/ui/components/PromptInput.jsx +515 -0
  63. package/src/ui/components/StreamingResponse.jsx +134 -0
  64. package/src/ui/components/ThinkingIndicator.jsx +365 -0
  65. package/src/ui/components/ToolExecution.jsx +714 -0
  66. package/src/ui/components/index.js +82 -0
  67. package/src/ui/context/TerminalContext.jsx +150 -0
  68. package/src/ui/context/index.js +13 -0
  69. package/src/ui/hooks/index.js +16 -0
  70. package/src/ui/hooks/useChatState.js +675 -0
  71. package/src/ui/hooks/useCommands.js +280 -0
  72. package/src/ui/hooks/useFileAttachments.js +216 -0
  73. package/src/ui/hooks/useKeyboardShortcuts.js +173 -0
  74. package/src/ui/hooks/useNotifications.js +185 -0
  75. package/src/ui/hooks/useTerminalSize.js +151 -0
  76. package/src/ui/hooks/useWebSocket.js +273 -0
  77. package/src/ui/index.js +94 -0
  78. package/src/ui/ink-runner.js +22 -0
  79. package/src/ui/utils/formatters.js +424 -0
  80. package/src/ui/utils/index.js +6 -0
  81. package/src/ui/utils/markdown.js +166 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Desktop notifications hook for Ink UI
3
+ * Provides notifications for long-running tasks
4
+ */
5
+
6
+ import { useCallback, useRef, useEffect } from 'react';
7
+
8
+ /**
9
+ * Check if desktop notifications are available
10
+ * @returns {boolean}
11
+ */
12
+ function areNotificationsAvailable() {
13
+ // Check for node-notifier or native notifications
14
+ try {
15
+ // In Node.js environment, we'll use terminal bell as fallback
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Send a terminal bell (audible notification)
24
+ */
25
+ function terminalBell() {
26
+ process.stdout.write('\x07');
27
+ }
28
+
29
+ /**
30
+ * Send a native notification (if available)
31
+ * @param {object} options - Notification options
32
+ */
33
+ async function sendNativeNotification({ title, message, sound = true }) {
34
+ try {
35
+ // Try to use node-notifier if available
36
+ const notifier = await import('node-notifier').catch(() => null);
37
+
38
+ if (notifier?.default?.notify) {
39
+ notifier.default.notify({
40
+ title: title || 'Otherwise',
41
+ message: message,
42
+ sound: sound,
43
+ wait: false,
44
+ });
45
+ return true;
46
+ }
47
+
48
+ // Fallback to terminal bell
49
+ if (sound) {
50
+ terminalBell();
51
+ }
52
+ return false;
53
+ } catch {
54
+ // Fallback to terminal bell
55
+ if (sound) {
56
+ terminalBell();
57
+ }
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Custom hook for notifications
64
+ * @param {object} options - Configuration options
65
+ * @returns {object} - Notification utilities
66
+ */
67
+ export function useNotifications(options = {}) {
68
+ const {
69
+ enabled = true,
70
+ soundEnabled = true,
71
+ minDuration = 30000, // Only notify for tasks > 30 seconds
72
+ notifyOnComplete = true,
73
+ notifyOnError = true,
74
+ } = options;
75
+
76
+ const taskStartTimes = useRef({});
77
+ const isAvailable = areNotificationsAvailable();
78
+
79
+ /**
80
+ * Start tracking a task for potential notification
81
+ * @param {string} taskId - Unique task identifier
82
+ */
83
+ const startTask = useCallback((taskId) => {
84
+ if (!enabled) return;
85
+ taskStartTimes.current[taskId] = Date.now();
86
+ }, [enabled]);
87
+
88
+ /**
89
+ * Complete a task and potentially send notification
90
+ * @param {string} taskId - Task identifier
91
+ * @param {object} options - Completion options
92
+ */
93
+ const completeTask = useCallback(async (taskId, {
94
+ success = true,
95
+ message = '',
96
+ forceNotify = false,
97
+ } = {}) => {
98
+ if (!enabled) return;
99
+
100
+ const startTime = taskStartTimes.current[taskId];
101
+ if (!startTime) return;
102
+
103
+ const duration = Date.now() - startTime;
104
+ delete taskStartTimes.current[taskId];
105
+
106
+ // Check if we should notify
107
+ const shouldNotify = forceNotify || duration >= minDuration;
108
+ if (!shouldNotify) return;
109
+
110
+ if (!success && notifyOnError) {
111
+ await sendNativeNotification({
112
+ title: 'Otherwise - Task Failed',
113
+ message: message || 'A task failed to complete',
114
+ sound: soundEnabled,
115
+ });
116
+ } else if (success && notifyOnComplete) {
117
+ await sendNativeNotification({
118
+ title: 'Otherwise - Task Complete',
119
+ message: message || 'Your task has completed',
120
+ sound: soundEnabled,
121
+ });
122
+ }
123
+ }, [enabled, minDuration, notifyOnComplete, notifyOnError, soundEnabled]);
124
+
125
+ /**
126
+ * Send an immediate notification
127
+ * @param {string} title - Notification title
128
+ * @param {string} message - Notification message
129
+ * @param {object} options - Additional options
130
+ */
131
+ const notify = useCallback(async (title, message, { sound = true } = {}) => {
132
+ if (!enabled) return false;
133
+
134
+ return sendNativeNotification({
135
+ title,
136
+ message,
137
+ sound: sound && soundEnabled,
138
+ });
139
+ }, [enabled, soundEnabled]);
140
+
141
+ /**
142
+ * Play just the bell sound
143
+ */
144
+ const bell = useCallback(() => {
145
+ if (soundEnabled) {
146
+ terminalBell();
147
+ }
148
+ }, [soundEnabled]);
149
+
150
+ // Cleanup on unmount
151
+ useEffect(() => {
152
+ return () => {
153
+ taskStartTimes.current = {};
154
+ };
155
+ }, []);
156
+
157
+ return {
158
+ isAvailable,
159
+ startTask,
160
+ completeTask,
161
+ notify,
162
+ bell,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Generation completion notification helper
168
+ * @param {object} stats - Generation stats
169
+ * @param {number} duration - Duration in ms
170
+ * @param {function} notify - Notify function
171
+ */
172
+ export async function notifyGenerationComplete(stats, duration, notify) {
173
+ if (duration < 30000) return; // Don't notify for quick generations
174
+
175
+ const tokenStr = stats?.numTokens ? `${stats.numTokens} tokens` : '';
176
+ const tpsStr = stats?.tps ? `${Math.round(stats.tps)} tok/s` : '';
177
+ const parts = [tokenStr, tpsStr].filter(Boolean).join(' • ');
178
+
179
+ await notify(
180
+ 'Generation Complete',
181
+ parts || 'AI has finished responding',
182
+ );
183
+ }
184
+
185
+ export default useNotifications;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * useTerminalSize hook
3
+ * Provides reactive terminal dimensions that update on resize
4
+ * Essential for responsive CLI rendering with Ink
5
+ */
6
+
7
+ import { useState, useEffect, useMemo } from 'react';
8
+
9
+ /**
10
+ * Default terminal dimensions fallback
11
+ */
12
+ const DEFAULT_COLUMNS = 80;
13
+ const DEFAULT_ROWS = 24;
14
+
15
+ /**
16
+ * Breakpoint thresholds for responsive layouts
17
+ */
18
+ const BREAKPOINTS = {
19
+ narrow: 60,
20
+ medium: 100,
21
+ wide: 140,
22
+ };
23
+
24
+ /**
25
+ * Get current terminal dimensions
26
+ * @returns {{columns: number, rows: number}}
27
+ */
28
+ function getTerminalSize() {
29
+ return {
30
+ columns: process.stdout.columns || DEFAULT_COLUMNS,
31
+ rows: process.stdout.rows || DEFAULT_ROWS,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Hook to track terminal size with automatic updates on resize
37
+ * @param {object} options - Configuration options
38
+ * @param {number} options.padding - Horizontal padding to subtract from content width (default: 4)
39
+ * @param {number} options.maxContentWidth - Maximum content width for readability (default: 120)
40
+ * @param {number} options.minContentWidth - Minimum content width (default: 40)
41
+ * @returns {object} Terminal size information and responsive utilities
42
+ */
43
+ export function useTerminalSize(options = {}) {
44
+ const {
45
+ padding = 4,
46
+ maxContentWidth = 120,
47
+ minContentWidth = 40,
48
+ } = options;
49
+
50
+ const [size, setSize] = useState(getTerminalSize);
51
+
52
+ useEffect(() => {
53
+ const handleResize = () => {
54
+ const newSize = getTerminalSize();
55
+ setSize(prev => {
56
+ // Only update if dimensions actually changed
57
+ if (prev.columns !== newSize.columns || prev.rows !== newSize.rows) {
58
+ return newSize;
59
+ }
60
+ return prev;
61
+ });
62
+ };
63
+
64
+ // Listen for resize events
65
+ process.stdout.on('resize', handleResize);
66
+
67
+ // Also check periodically in case resize event is missed
68
+ // (some terminals don't emit resize properly)
69
+ const interval = setInterval(handleResize, 1000);
70
+
71
+ return () => {
72
+ process.stdout.off('resize', handleResize);
73
+ clearInterval(interval);
74
+ };
75
+ }, []);
76
+
77
+ // Compute responsive values
78
+ const responsive = useMemo(() => {
79
+ const { columns, rows } = size;
80
+
81
+ // Responsive breakpoints
82
+ const isNarrow = columns < BREAKPOINTS.narrow;
83
+ const isMedium = columns >= BREAKPOINTS.narrow && columns < BREAKPOINTS.medium;
84
+ const isWide = columns >= BREAKPOINTS.medium;
85
+ const isExtraWide = columns >= BREAKPOINTS.wide;
86
+
87
+ // Content width calculations
88
+ const rawContentWidth = columns - padding;
89
+ const contentWidth = Math.max(minContentWidth, Math.min(rawContentWidth, maxContentWidth));
90
+
91
+ // For text that should use more width on wide terminals
92
+ const textWidth = Math.max(minContentWidth, Math.min(rawContentWidth, 100));
93
+
94
+ // For UI elements like borders and dividers
95
+ const uiWidth = Math.max(30, Math.min(rawContentWidth, 60));
96
+
97
+ // Compact mode for very narrow terminals
98
+ const useCompactMode = columns < 50;
99
+
100
+ return {
101
+ columns,
102
+ rows,
103
+ isNarrow,
104
+ isMedium,
105
+ isWide,
106
+ isExtraWide,
107
+ contentWidth,
108
+ textWidth,
109
+ uiWidth,
110
+ useCompactMode,
111
+ // Raw values for custom calculations
112
+ rawWidth: columns,
113
+ rawHeight: rows,
114
+ };
115
+ }, [size, padding, maxContentWidth, minContentWidth]);
116
+
117
+ return responsive;
118
+ }
119
+
120
+ /**
121
+ * Get terminal width synchronously (for non-React contexts)
122
+ * @returns {number} Current terminal width
123
+ */
124
+ export function getTerminalWidth() {
125
+ return process.stdout.columns || DEFAULT_COLUMNS;
126
+ }
127
+
128
+ /**
129
+ * Get terminal height synchronously (for non-React contexts)
130
+ * @returns {number} Current terminal height
131
+ */
132
+ export function getTerminalHeight() {
133
+ return process.stdout.rows || DEFAULT_ROWS;
134
+ }
135
+
136
+ /**
137
+ * Calculate responsive width based on terminal size
138
+ * @param {number} baseWidth - Base width at 100 columns
139
+ * @param {object} options - Options
140
+ * @param {number} options.min - Minimum width
141
+ * @param {number} options.max - Maximum width
142
+ * @returns {number} Scaled width
143
+ */
144
+ export function calculateResponsiveWidth(baseWidth, options = {}) {
145
+ const { min = 20, max = baseWidth * 1.5 } = options;
146
+ const terminalWidth = getTerminalWidth();
147
+ const scale = terminalWidth / 100;
148
+ return Math.max(min, Math.min(Math.floor(baseWidth * scale), max));
149
+ }
150
+
151
+ export default useTerminalSize;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * WebSocket connection hook for Ink UI
3
+ * Manages real-time communication with the Otherwise server
4
+ */
5
+
6
+ import { useState, useEffect, useRef, useCallback } from 'react';
7
+ import { WebSocket } from 'ws';
8
+
9
+ /**
10
+ * WebSocket connection states
11
+ */
12
+ export const ConnectionState = {
13
+ CONNECTING: 'connecting',
14
+ CONNECTED: 'connected',
15
+ DISCONNECTED: 'disconnected',
16
+ RECONNECTING: 'reconnecting',
17
+ ERROR: 'error',
18
+ };
19
+
20
+ /**
21
+ * Custom hook for WebSocket connection management
22
+ * @param {string} serverUrl - Base server URL (http://localhost:3000)
23
+ * @param {object} options - Configuration options
24
+ * @returns {object} - WebSocket state and methods
25
+ */
26
+ export function useWebSocket(serverUrl, options = {}) {
27
+ const {
28
+ autoConnect = true,
29
+ autoReconnect = true,
30
+ reconnectDelay = 2000,
31
+ maxReconnectAttempts = 10,
32
+ onMessage,
33
+ onConnect,
34
+ onDisconnect,
35
+ onError,
36
+ } = options;
37
+
38
+ const wsUrl = serverUrl.replace('http', 'ws') + '/ws';
39
+
40
+ const [connectionState, setConnectionState] = useState(ConnectionState.DISCONNECTED);
41
+ const [error, setError] = useState(null);
42
+
43
+ const wsRef = useRef(null);
44
+ const reconnectAttemptsRef = useRef(0);
45
+ const reconnectTimeoutRef = useRef(null);
46
+ const mountedRef = useRef(true);
47
+ const hasConnectedRef = useRef(false); // Track if we've ever connected
48
+ const connectionStateRef = useRef(connectionState); // Track current state in ref
49
+
50
+ // Keep ref in sync with state
51
+ useEffect(() => {
52
+ connectionStateRef.current = connectionState;
53
+ }, [connectionState]);
54
+
55
+ /**
56
+ * Schedule a reconnection attempt
57
+ */
58
+ const scheduleReconnect = useCallback(() => {
59
+ if (!autoReconnect) return;
60
+ if (reconnectAttemptsRef.current >= maxReconnectAttempts) return;
61
+ if (!mountedRef.current) return;
62
+
63
+ reconnectAttemptsRef.current++;
64
+ setConnectionState(ConnectionState.RECONNECTING);
65
+
66
+ reconnectTimeoutRef.current = setTimeout(() => {
67
+ if (mountedRef.current) {
68
+ if (wsRef.current) {
69
+ wsRef.current.removeAllListeners();
70
+ wsRef.current = null;
71
+ }
72
+ setConnectionState(ConnectionState.CONNECTING);
73
+ createConnection();
74
+ }
75
+ }, reconnectDelay);
76
+ }, [autoReconnect, maxReconnectAttempts, reconnectDelay]);
77
+
78
+ /**
79
+ * Create WebSocket connection
80
+ */
81
+ const createConnection = useCallback(() => {
82
+ try {
83
+ const ws = new WebSocket(wsUrl);
84
+ wsRef.current = ws;
85
+
86
+ ws.on('open', () => {
87
+ if (!mountedRef.current) return;
88
+
89
+ hasConnectedRef.current = true;
90
+ reconnectAttemptsRef.current = 0;
91
+ setConnectionState(ConnectionState.CONNECTED);
92
+ onConnect?.();
93
+ });
94
+
95
+ ws.on('message', (data) => {
96
+ if (!mountedRef.current) return;
97
+
98
+ try {
99
+ const message = JSON.parse(data.toString());
100
+ onMessage?.(message);
101
+ } catch (e) {
102
+ // Ignore parse errors
103
+ }
104
+ });
105
+
106
+ ws.on('close', () => {
107
+ if (!mountedRef.current) return;
108
+
109
+ const wasConnected = connectionStateRef.current === ConnectionState.CONNECTED;
110
+ setConnectionState(ConnectionState.DISCONNECTED);
111
+
112
+ if (wasConnected) {
113
+ onDisconnect?.();
114
+ }
115
+
116
+ // Always try to reconnect (whether initial connect failed or we disconnected)
117
+ scheduleReconnect();
118
+ });
119
+
120
+ ws.on('error', (err) => {
121
+ if (!mountedRef.current) return;
122
+
123
+ // Only report errors if we were connected
124
+ if (hasConnectedRef.current) {
125
+ setError(err.message);
126
+ onError?.(err);
127
+ }
128
+ // The 'close' event will fire after error, which will trigger reconnect
129
+ });
130
+ } catch (err) {
131
+ // Connection creation failed - schedule retry
132
+ scheduleReconnect();
133
+ }
134
+ }, [wsUrl, onMessage, onConnect, onDisconnect, onError, scheduleReconnect]);
135
+
136
+ /**
137
+ * Connect to WebSocket server
138
+ */
139
+ const connect = useCallback(() => {
140
+ // Don't connect if already connected or connecting
141
+ if (wsRef.current?.readyState === WebSocket.OPEN) return;
142
+ if (wsRef.current?.readyState === WebSocket.CONNECTING) return;
143
+
144
+ // Clean up existing socket
145
+ if (wsRef.current) {
146
+ wsRef.current.removeAllListeners();
147
+ wsRef.current = null;
148
+ }
149
+
150
+ setConnectionState(ConnectionState.CONNECTING);
151
+ setError(null);
152
+ reconnectAttemptsRef.current = 0;
153
+
154
+ createConnection();
155
+ }, [createConnection]);
156
+
157
+ /**
158
+ * Disconnect from WebSocket server
159
+ */
160
+ const disconnect = useCallback(() => {
161
+ // Cancel any pending reconnect
162
+ if (reconnectTimeoutRef.current) {
163
+ clearTimeout(reconnectTimeoutRef.current);
164
+ reconnectTimeoutRef.current = null;
165
+ }
166
+
167
+ // Close and clean up socket
168
+ if (wsRef.current) {
169
+ wsRef.current.removeAllListeners();
170
+ try {
171
+ wsRef.current.close();
172
+ } catch (e) {
173
+ // Ignore close errors
174
+ }
175
+ wsRef.current = null;
176
+ }
177
+
178
+ setConnectionState(ConnectionState.DISCONNECTED);
179
+ }, []);
180
+
181
+ /**
182
+ * Send a message through WebSocket
183
+ * @param {object} message - Message to send
184
+ * @returns {boolean} - Success status
185
+ */
186
+ const send = useCallback((message) => {
187
+ if (wsRef.current?.readyState !== WebSocket.OPEN) {
188
+ return false;
189
+ }
190
+
191
+ try {
192
+ wsRef.current.send(JSON.stringify(message));
193
+ return true;
194
+ } catch (err) {
195
+ setError(err.message);
196
+ return false;
197
+ }
198
+ }, []);
199
+
200
+ /**
201
+ * Send a chat message
202
+ * @param {object} payload - Chat payload
203
+ */
204
+ const sendChatMessage = useCallback((payload) => {
205
+ return send({
206
+ type: 'chat',
207
+ payload,
208
+ });
209
+ }, [send]);
210
+
211
+ /**
212
+ * Send a stop generation request
213
+ * @param {number} chatId - Chat ID to stop
214
+ */
215
+ const sendStop = useCallback((chatId) => {
216
+ return send({
217
+ type: 'stop',
218
+ chatId,
219
+ });
220
+ }, [send]);
221
+
222
+ /**
223
+ * Broadcast chat selection to other clients
224
+ * @param {number|null} chatId - Selected chat ID
225
+ */
226
+ const selectChat = useCallback((chatId) => {
227
+ return send({
228
+ type: 'select_chat',
229
+ chatId,
230
+ });
231
+ }, [send]);
232
+
233
+ // Auto-connect on mount
234
+ useEffect(() => {
235
+ mountedRef.current = true;
236
+
237
+ if (autoConnect) {
238
+ // Delay to ensure server is ready (server starts concurrently with Ink UI)
239
+ const timer = setTimeout(() => {
240
+ if (mountedRef.current) {
241
+ connect();
242
+ }
243
+ }, 500);
244
+
245
+ return () => {
246
+ clearTimeout(timer);
247
+ mountedRef.current = false;
248
+ disconnect();
249
+ };
250
+ }
251
+
252
+ return () => {
253
+ mountedRef.current = false;
254
+ disconnect();
255
+ };
256
+ }, []); // Only run on mount/unmount - don't depend on connect/disconnect
257
+
258
+ return {
259
+ connectionState,
260
+ isConnected: connectionState === ConnectionState.CONNECTED,
261
+ isConnecting: connectionState === ConnectionState.CONNECTING,
262
+ isReconnecting: connectionState === ConnectionState.RECONNECTING,
263
+ error,
264
+ connect,
265
+ disconnect,
266
+ send,
267
+ sendChatMessage,
268
+ sendStop,
269
+ selectChat,
270
+ };
271
+ }
272
+
273
+ export default useWebSocket;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Ink UI entry point
3
+ * Provides a clean interface to the Ink-based React UI
4
+ *
5
+ * Note: This file uses dynamic import with tsx loader for JSX transpilation.
6
+ * The UI components use JSX syntax and are loaded at runtime.
7
+ */
8
+
9
+ import { render } from 'ink';
10
+ import React from 'react';
11
+ import chalk from 'chalk';
12
+
13
+ // Re-export hooks and utilities (these don't use JSX)
14
+ export * from './hooks/index.js';
15
+ export * from './utils/index.js';
16
+
17
+ /**
18
+ * Dynamically load JSX components using tsx
19
+ * Falls back to a simple UI if tsx loader isn't available
20
+ */
21
+ async function loadComponents() {
22
+ try {
23
+ // Try to import JSX component
24
+ // This will work if tsx loader is registered or if running under tsx
25
+ const { App } = await import('./components/App.jsx');
26
+ return { App };
27
+ } catch (err) {
28
+ // Fallback: return a simple placeholder component
29
+ console.error('Failed to load Ink components:', err.message);
30
+
31
+ // Return a simple fallback component
32
+ const FallbackApp = () => {
33
+ return React.createElement('text', { color: 'red' },
34
+ 'Error: JSX components could not be loaded.'
35
+ );
36
+ };
37
+
38
+ return { App: FallbackApp };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Start the Ink-based CLI
44
+ * @param {object} options - Configuration options
45
+ * @param {string} options.serverUrl - Server URL to connect to
46
+ * @param {boolean} options.showBanner - Whether to show the welcome banner
47
+ * @returns {Promise<object>} - Ink instance with rerender, unmount, waitUntilExit, clear methods
48
+ */
49
+ export async function startInkCli(options = {}) {
50
+ const {
51
+ serverUrl = 'http://localhost:3000',
52
+ showBanner = true,
53
+ isRemoteMode = false,
54
+ } = options;
55
+
56
+ // Clear terminal and set title before rendering
57
+ // This ensures a clean slate for Ink to manage
58
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear screen and move cursor to top-left
59
+ process.stdout.write('\x1b]0;Otherwise\x07\x1b]2;Otherwise\x07'); // Set title
60
+
61
+ // Load the App component
62
+ const { App } = await loadComponents();
63
+
64
+ // Create React element
65
+ const element = React.createElement(App, { serverUrl, showBanner, isRemoteMode });
66
+
67
+ // Render the app with optimized settings
68
+ const instance = render(element, {
69
+ exitOnCtrlC: false, // We handle Ctrl+C ourselves
70
+ patchConsole: true, // Intercept console.log to not interfere with UI
71
+ });
72
+
73
+ return instance;
74
+ }
75
+
76
+ /**
77
+ * Main entry point when run directly
78
+ */
79
+ export async function main() {
80
+ const instance = await startInkCli({
81
+ serverUrl: process.env.SERVER_URL || 'http://localhost:3000',
82
+ showBanner: true,
83
+ });
84
+
85
+ // Wait for exit
86
+ await instance.waitUntilExit();
87
+
88
+ console.log(chalk.dim.italic('\nGoodbye'));
89
+ }
90
+
91
+ export default {
92
+ startInkCli,
93
+ main,
94
+ };