codevf 1.0.0 → 1.0.2

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 (203) hide show
  1. package/LICENSE +30 -21
  2. package/README.md +7 -2
  3. package/bin/codevf-mcp.js +11 -0
  4. package/dist/commands/fix.d.ts +5 -1
  5. package/dist/commands/fix.d.ts.map +1 -1
  6. package/dist/commands/fix.js +170 -13
  7. package/dist/commands/fix.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +72 -2
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/mcp-tools.d.ts +17 -0
  12. package/dist/commands/mcp-tools.d.ts.map +1 -0
  13. package/dist/commands/mcp-tools.js +237 -0
  14. package/dist/commands/mcp-tools.js.map +1 -0
  15. package/dist/commands/setup.d.ts +8 -0
  16. package/dist/commands/setup.d.ts.map +1 -0
  17. package/dist/commands/setup.js +250 -0
  18. package/dist/commands/setup.js.map +1 -0
  19. package/dist/commands/welcome.d.ts +9 -0
  20. package/dist/commands/welcome.d.ts.map +1 -0
  21. package/dist/commands/welcome.js +175 -0
  22. package/dist/commands/welcome.js.map +1 -0
  23. package/dist/index.js +263 -207
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/api/client.d.ts +28 -0
  26. package/dist/lib/api/client.d.ts.map +1 -0
  27. package/dist/lib/api/client.js +66 -0
  28. package/dist/lib/api/client.js.map +1 -0
  29. package/dist/lib/api/projects.d.ts +32 -0
  30. package/dist/lib/api/projects.d.ts.map +1 -0
  31. package/dist/lib/api/projects.js +61 -0
  32. package/dist/lib/api/projects.js.map +1 -0
  33. package/dist/lib/api/tasks.d.ts +36 -0
  34. package/dist/lib/api/tasks.d.ts.map +1 -0
  35. package/dist/lib/api/tasks.js +62 -0
  36. package/dist/lib/api/tasks.js.map +1 -0
  37. package/dist/lib/api/websocket.d.ts +50 -0
  38. package/dist/lib/api/websocket.d.ts.map +1 -0
  39. package/dist/lib/api/websocket.js +153 -0
  40. package/dist/lib/api/websocket.js.map +1 -0
  41. package/dist/lib/auth/oauth-flow.d.ts +37 -0
  42. package/dist/lib/auth/oauth-flow.d.ts.map +1 -0
  43. package/dist/lib/auth/oauth-flow.js +119 -0
  44. package/dist/lib/auth/oauth-flow.js.map +1 -0
  45. package/dist/lib/auth/token-manager.d.ts +26 -0
  46. package/dist/lib/auth/token-manager.d.ts.map +1 -0
  47. package/dist/lib/auth/token-manager.js +87 -0
  48. package/dist/lib/auth/token-manager.js.map +1 -0
  49. package/dist/lib/config/manager.d.ts +50 -0
  50. package/dist/lib/config/manager.d.ts.map +1 -0
  51. package/dist/lib/config/manager.js +84 -0
  52. package/dist/lib/config/manager.js.map +1 -0
  53. package/dist/lib/utils/errors.d.ts +28 -0
  54. package/dist/lib/utils/errors.d.ts.map +1 -0
  55. package/dist/lib/utils/errors.js +44 -0
  56. package/dist/lib/utils/errors.js.map +1 -0
  57. package/dist/lib/utils/logger.d.ts +20 -0
  58. package/dist/lib/utils/logger.d.ts.map +1 -0
  59. package/dist/lib/utils/logger.js +40 -0
  60. package/dist/lib/utils/logger.js.map +1 -0
  61. package/dist/mcp/index.d.ts +7 -0
  62. package/dist/mcp/index.d.ts.map +1 -0
  63. package/dist/mcp/index.js +160 -0
  64. package/dist/mcp/index.js.map +1 -0
  65. package/dist/mcp/tools/chat.d.ts +30 -0
  66. package/dist/mcp/tools/chat.d.ts.map +1 -0
  67. package/dist/mcp/tools/chat.js +82 -0
  68. package/dist/mcp/tools/chat.js.map +1 -0
  69. package/dist/mcp/tools/instant.d.ts +38 -0
  70. package/dist/mcp/tools/instant.d.ts.map +1 -0
  71. package/dist/mcp/tools/instant.js +106 -0
  72. package/dist/mcp/tools/instant.js.map +1 -0
  73. package/dist/modules/aiAgent.d.ts +75 -0
  74. package/dist/modules/aiAgent.d.ts.map +1 -0
  75. package/dist/modules/aiAgent.js +707 -0
  76. package/dist/modules/aiAgent.js.map +1 -0
  77. package/dist/modules/api.d.ts +7 -0
  78. package/dist/modules/api.d.ts.map +1 -1
  79. package/dist/modules/api.js +13 -4
  80. package/dist/modules/api.js.map +1 -1
  81. package/dist/modules/commandHandler.d.ts +40 -0
  82. package/dist/modules/commandHandler.d.ts.map +1 -0
  83. package/dist/modules/commandHandler.js +328 -0
  84. package/dist/modules/commandHandler.js.map +1 -0
  85. package/dist/modules/config.d.ts +2 -0
  86. package/dist/modules/config.d.ts.map +1 -1
  87. package/dist/modules/config.js +9 -0
  88. package/dist/modules/config.js.map +1 -1
  89. package/dist/modules/constants.d.ts +83 -0
  90. package/dist/modules/constants.d.ts.map +1 -0
  91. package/dist/modules/constants.js +75 -0
  92. package/dist/modules/constants.js.map +1 -0
  93. package/dist/modules/permissions.d.ts +14 -0
  94. package/dist/modules/permissions.d.ts.map +1 -1
  95. package/dist/modules/permissions.js +94 -0
  96. package/dist/modules/permissions.js.map +1 -1
  97. package/dist/modules/toolRegistry.d.ts +50 -0
  98. package/dist/modules/toolRegistry.d.ts.map +1 -0
  99. package/dist/modules/toolRegistry.js +114 -0
  100. package/dist/modules/toolRegistry.js.map +1 -0
  101. package/dist/modules/tunnel.d.ts +33 -0
  102. package/dist/modules/tunnel.d.ts.map +1 -0
  103. package/dist/modules/tunnel.js +79 -0
  104. package/dist/modules/tunnel.js.map +1 -0
  105. package/dist/modules/vibeHelper.d.ts +16 -0
  106. package/dist/modules/vibeHelper.d.ts.map +1 -0
  107. package/dist/modules/vibeHelper.js +38 -0
  108. package/dist/modules/vibeHelper.js.map +1 -0
  109. package/dist/modules/websocket.d.ts +9 -0
  110. package/dist/modules/websocket.d.ts.map +1 -1
  111. package/dist/modules/websocket.js +70 -0
  112. package/dist/modules/websocket.js.map +1 -1
  113. package/dist/tools/consultEngineer.d.ts +13 -0
  114. package/dist/tools/consultEngineer.d.ts.map +1 -0
  115. package/dist/tools/consultEngineer.js +161 -0
  116. package/dist/tools/consultEngineer.js.map +1 -0
  117. package/dist/tools/realtimeChat.d.ts +9 -0
  118. package/dist/tools/realtimeChat.d.ts.map +1 -0
  119. package/dist/tools/realtimeChat.js +101 -0
  120. package/dist/tools/realtimeChat.js.map +1 -0
  121. package/dist/types/index.d.ts +183 -0
  122. package/dist/types/index.d.ts.map +1 -1
  123. package/dist/types/index.js.map +1 -1
  124. package/dist/ui/InteractiveApp.d.ts +13 -0
  125. package/dist/ui/InteractiveApp.d.ts.map +1 -0
  126. package/dist/ui/InteractiveApp.js +84 -0
  127. package/dist/ui/InteractiveApp.js.map +1 -0
  128. package/dist/ui/InteractivePrompt.d.ts +53 -0
  129. package/dist/ui/InteractivePrompt.d.ts.map +1 -0
  130. package/dist/ui/InteractivePrompt.js +422 -0
  131. package/dist/ui/InteractivePrompt.js.map +1 -0
  132. package/dist/ui/LiveSession.d.ts +2 -0
  133. package/dist/ui/LiveSession.d.ts.map +1 -1
  134. package/dist/ui/LiveSession.js +461 -180
  135. package/dist/ui/LiveSession.js.map +1 -1
  136. package/dist/ui/PromptInput.d.ts +14 -0
  137. package/dist/ui/PromptInput.d.ts.map +1 -0
  138. package/dist/ui/PromptInput.js +206 -0
  139. package/dist/ui/PromptInput.js.map +1 -0
  140. package/dist/ui/SessionUI.d.ts +40 -0
  141. package/dist/ui/SessionUI.d.ts.map +1 -0
  142. package/dist/ui/SessionUI.js +218 -0
  143. package/dist/ui/SessionUI.js.map +1 -0
  144. package/dist/ui/input/Command.d.ts +22 -0
  145. package/dist/ui/input/Command.d.ts.map +1 -0
  146. package/dist/ui/input/Command.js +30 -0
  147. package/dist/ui/input/Command.js.map +1 -0
  148. package/dist/ui/input/CustomInput.d.ts +15 -0
  149. package/dist/ui/input/CustomInput.d.ts.map +1 -0
  150. package/dist/ui/input/CustomInput.js +182 -0
  151. package/dist/ui/input/CustomInput.js.map +1 -0
  152. package/dist/ui/input/handlers/handleCursor.d.ts +22 -0
  153. package/dist/ui/input/handlers/handleCursor.d.ts.map +1 -0
  154. package/dist/ui/input/handlers/handleCursor.js +53 -0
  155. package/dist/ui/input/handlers/handleCursor.js.map +1 -0
  156. package/dist/ui/input/handlers/handleEdit.d.ts +18 -0
  157. package/dist/ui/input/handlers/handleEdit.d.ts.map +1 -0
  158. package/dist/ui/input/handlers/handleEdit.js +55 -0
  159. package/dist/ui/input/handlers/handleEdit.js.map +1 -0
  160. package/dist/ui/input/handlers/handleHistory.d.ts +18 -0
  161. package/dist/ui/input/handlers/handleHistory.d.ts.map +1 -0
  162. package/dist/ui/input/handlers/handleHistory.js +85 -0
  163. package/dist/ui/input/handlers/handleHistory.js.map +1 -0
  164. package/dist/ui/input/handlers/handlePaste.d.ts +19 -0
  165. package/dist/ui/input/handlers/handlePaste.d.ts.map +1 -0
  166. package/dist/ui/input/handlers/handlePaste.js +49 -0
  167. package/dist/ui/input/handlers/handlePaste.js.map +1 -0
  168. package/dist/ui/input/handlers/handleSubmit.d.ts +18 -0
  169. package/dist/ui/input/handlers/handleSubmit.d.ts.map +1 -0
  170. package/dist/ui/input/handlers/handleSubmit.js +39 -0
  171. package/dist/ui/input/handlers/handleSubmit.js.map +1 -0
  172. package/dist/ui/input/helpers.d.ts +4 -0
  173. package/dist/ui/input/helpers.d.ts.map +1 -0
  174. package/dist/ui/input/helpers.js +13 -0
  175. package/dist/ui/input/helpers.js.map +1 -0
  176. package/dist/ui/input/keyMatchers.d.ts +14 -0
  177. package/dist/ui/input/keyMatchers.d.ts.map +1 -0
  178. package/dist/ui/input/keyMatchers.js +49 -0
  179. package/dist/ui/input/keyMatchers.js.map +1 -0
  180. package/dist/ui/input/types.d.ts +33 -0
  181. package/dist/ui/input/types.d.ts.map +1 -0
  182. package/dist/ui/input/types.js +2 -0
  183. package/dist/ui/input/types.js.map +1 -0
  184. package/dist/ui/promptWithModes.d.ts +12 -0
  185. package/dist/ui/promptWithModes.d.ts.map +1 -0
  186. package/dist/ui/promptWithModes.js +24 -0
  187. package/dist/ui/promptWithModes.js.map +1 -0
  188. package/dist/ui/renderPrompt.d.ts +12 -0
  189. package/dist/ui/renderPrompt.d.ts.map +1 -0
  190. package/dist/ui/renderPrompt.js +14 -0
  191. package/dist/ui/renderPrompt.js.map +1 -0
  192. package/dist/ui/simplePrompt.d.ts +7 -0
  193. package/dist/ui/simplePrompt.d.ts.map +1 -0
  194. package/dist/ui/simplePrompt.js +38 -0
  195. package/dist/ui/simplePrompt.js.map +1 -0
  196. package/dist/ui/spinner.d.ts +7 -0
  197. package/dist/ui/spinner.d.ts.map +1 -0
  198. package/dist/ui/spinner.js +13 -0
  199. package/dist/ui/spinner.js.map +1 -0
  200. package/package.json +36 -26
  201. package/ARCHITECTURE.md +0 -285
  202. package/BUILD_SUMMARY.md +0 -340
  203. package/QUICKSTART.md +0 -180
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { Box, Text, useInput, useApp } from 'ink';
3
3
  import * as fs from 'fs';
4
4
  import { exec } from 'child_process';
@@ -6,44 +6,189 @@ import { promisify } from 'util';
6
6
  import { useRef } from 'react';
7
7
  const execAsync = promisify(exec);
8
8
  const COMMANDS = [
9
- { cmd: '/end', description: 'End the session' },
10
- { cmd: '/help', description: 'Show available commands' },
11
- { cmd: '/?', description: 'Show available commands' },
9
+ { cmd: '/hybrid', description: 'Hybrid mode (AI → Human) (active)' },
10
+ { cmd: '/ai', description: 'Use AI mode only' },
11
+ { cmd: '/human', description: 'Request human engineer' },
12
12
  { cmd: '/shell', description: 'Switch to local shell (not shared)' },
13
+ { cmd: '/tunnel <port>', description: 'Share a local port over the internet' },
14
+ { cmd: '/close-tunnel', description: 'Close the active tunnel' },
13
15
  { cmd: '/resume', description: 'Return from shell to session' },
16
+ { cmd: '/?', description: 'Show all commands' },
17
+ { cmd: '/help', description: 'Show all commands' },
18
+ { cmd: '/end', description: 'End the session' },
14
19
  ];
15
- export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, }) => {
20
+ export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, tunnelManager, }) => {
16
21
  const [messages, setMessages] = useState([]);
17
22
  const [engineerName, setEngineerName] = useState(null);
18
23
  const [engineerTitle, setEngineerTitle] = useState('');
19
24
  const [creditsUsed, setCreditsUsed] = useState(0);
20
- const [sessionDuration, setSessionDuration] = useState(0);
25
+ const [_sessionDuration, setSessionDuration] = useState(0);
21
26
  const [inputBuffer, setInputBuffer] = useState('');
22
27
  const [isProcessingRequest, setIsProcessingRequest] = useState(false);
23
28
  const { exit } = useApp();
24
29
  const endingRef = useRef(false);
25
30
  const [mode, setMode] = useState('session');
31
+ const [isPasteMode, setIsPasteMode] = useState(false);
32
+ const [activeTunnel, setActiveTunnel] = useState(null);
33
+ const lastInputTimeRef = useRef(0);
34
+ const blockReturnsUntilRef = useRef(0);
35
+ const shareTunnel = useCallback(async (port, subdomain, reason, requestedByEngineer = false) => {
36
+ if (requestedByEngineer) {
37
+ const approved = await permissionManager.requestTunnelPermission(port, reason);
38
+ if (!approved) {
39
+ setMessages((prev) => [
40
+ ...prev,
41
+ {
42
+ timestamp: new Date().toISOString(),
43
+ sender: 'system',
44
+ content: `✗ Tunnel request denied for port ${port}`,
45
+ },
46
+ ]);
47
+ try {
48
+ wsClient.send({
49
+ type: 'tunnel_error',
50
+ timestamp: new Date().toISOString(),
51
+ payload: { port, message: 'Denied by customer' },
52
+ });
53
+ }
54
+ catch (error) {
55
+ // best-effort
56
+ }
57
+ return null;
58
+ }
59
+ }
60
+ setIsProcessingRequest(true);
61
+ try {
62
+ const tunnel = await tunnelManager.createTunnel({ port, subdomain, taskId });
63
+ setActiveTunnel(tunnel);
64
+ const tunnelMessage = `🔗 Tunnel shared: ${tunnel.url} (port ${tunnel.port})`;
65
+ setMessages((prev) => [
66
+ ...prev,
67
+ {
68
+ timestamp: new Date().toISOString(),
69
+ sender: 'system',
70
+ content: tunnelMessage,
71
+ },
72
+ ]);
73
+ try {
74
+ wsClient.send({
75
+ type: 'tunnel_shared',
76
+ timestamp: new Date().toISOString(),
77
+ payload: { port: tunnel.port, url: tunnel.url },
78
+ });
79
+ }
80
+ catch (error) {
81
+ // best-effort to notify engineer
82
+ }
83
+ try {
84
+ await apiClient.sendMessage(taskId, tunnelMessage);
85
+ }
86
+ catch (error) {
87
+ // sending message is best-effort; continue
88
+ }
89
+ return tunnel;
90
+ }
91
+ catch (error) {
92
+ const message = error?.message || 'Failed to create tunnel';
93
+ setMessages((prev) => [
94
+ ...prev,
95
+ {
96
+ timestamp: new Date().toISOString(),
97
+ sender: 'system',
98
+ content: `✗ ${message}`,
99
+ },
100
+ ]);
101
+ try {
102
+ wsClient.send({
103
+ type: 'tunnel_error',
104
+ timestamp: new Date().toISOString(),
105
+ payload: { port, message },
106
+ });
107
+ }
108
+ catch (sendError) {
109
+ // ignore
110
+ }
111
+ return null;
112
+ }
113
+ finally {
114
+ setIsProcessingRequest(false);
115
+ }
116
+ }, [apiClient, permissionManager, taskId, tunnelManager, wsClient]);
117
+ const closeActiveTunnel = useCallback(async () => {
118
+ if (!activeTunnel) {
119
+ setMessages((prev) => [
120
+ ...prev,
121
+ {
122
+ timestamp: new Date().toISOString(),
123
+ sender: 'system',
124
+ content: 'No active tunnel to close.',
125
+ },
126
+ ]);
127
+ return;
128
+ }
129
+ setIsProcessingRequest(true);
130
+ try {
131
+ await tunnelManager.closeTunnel();
132
+ setActiveTunnel(null);
133
+ setMessages((prev) => [
134
+ ...prev,
135
+ {
136
+ timestamp: new Date().toISOString(),
137
+ sender: 'system',
138
+ content: `Tunnel closed for port ${activeTunnel.port}`,
139
+ },
140
+ ]);
141
+ }
142
+ finally {
143
+ setIsProcessingRequest(false);
144
+ }
145
+ }, [activeTunnel, tunnelManager]);
146
+ useEffect(() => {
147
+ // Enable bracketed paste mode
148
+ process.stdout.write('\x1b[?2004h');
149
+ return () => {
150
+ // Disable bracketed paste mode on unmount
151
+ process.stdout.write('\x1b[?2004l');
152
+ };
153
+ }, []);
26
154
  useEffect(() => {
27
- // Add initial message
155
+ // Add initial message with status and commands
156
+ const initMessage = `[initialized] • 🤖 [Hybrid+Vibe: 240 credits max]
157
+
158
+ AI=OpenCode (free w/ limits), Vibe=2-3 credits, Human=2 credits/min • /? for info
159
+
160
+ Quick commands:
161
+ /hybrid - Hybrid mode (AI → Human) (active)
162
+ /ai - Use AI mode only
163
+ /human - Request human engineer
164
+ /shell - Local shell (not shared)
165
+ /tunnel <port> - Share a local port via secure tunnel
166
+ /? - Show all commands
167
+ /end - End session
168
+
169
+ 🔗 Connecting to engineer...`;
28
170
  setMessages([
29
171
  {
30
172
  timestamp: new Date().toISOString(),
31
173
  sender: 'system',
32
- content: 'Connecting to engineer... (type /? for commands, /shell for local shell, /end to close)',
174
+ content: initMessage,
33
175
  },
34
176
  ]);
35
177
  // WebSocket event handlers
36
178
  wsClient.on('engineer_connected', (data) => {
37
179
  setEngineerName(data.payload.engineerName);
38
180
  setEngineerTitle(data.payload.engineerTitle);
39
- setMessages((prev) => [
40
- ...prev,
41
- {
42
- timestamp: new Date().toISOString(),
43
- sender: 'system',
44
- content: `Engineer connected: ${data.payload.engineerName} (${data.payload.engineerTitle})`,
45
- },
46
- ]);
181
+ // Only add message if not already connected (prevent duplicates)
182
+ if (!engineerName) {
183
+ setMessages((prev) => [
184
+ ...prev,
185
+ {
186
+ timestamp: new Date().toISOString(),
187
+ sender: 'system',
188
+ content: 'Engineer connected',
189
+ },
190
+ ]);
191
+ }
47
192
  });
48
193
  wsClient.on('engineer_message', (data) => {
49
194
  setMessages((prev) => [
@@ -162,6 +307,10 @@ export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, })
162
307
  }
163
308
  setIsProcessingRequest(false);
164
309
  });
310
+ wsClient.on('request_tunnel', async (data) => {
311
+ const { port, reason, subdomain } = data.payload;
312
+ await shareTunnel(port, subdomain, reason, true);
313
+ });
165
314
  wsClient.on('screenshare_request', (data) => {
166
315
  setMessages((prev) => [
167
316
  ...prev,
@@ -183,14 +332,18 @@ export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, })
183
332
  ]);
184
333
  });
185
334
  wsClient.on('session_end', async (data) => {
335
+ const endedBy = data?.payload?.endedBy || 'engineer';
336
+ const endMessage = endedBy === 'customer' ? 'Session ended by User' : 'Session ended by engineer';
186
337
  setMessages((prev) => [
187
338
  ...prev,
188
339
  {
189
340
  timestamp: new Date().toISOString(),
190
341
  sender: 'system',
191
- content: 'Session ended by engineer',
342
+ content: endMessage,
192
343
  },
193
344
  ]);
345
+ // Ensure tunnels are closed when session ends
346
+ void tunnelManager.closeTunnel().then(() => setActiveTunnel(null));
194
347
  // Show summary and exit after a delay
195
348
  setTimeout(() => exit(), 2000);
196
349
  });
@@ -214,193 +367,317 @@ export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, })
214
367
  },
215
368
  ]);
216
369
  });
370
+ wsClient.on('disconnected', () => {
371
+ setMessages((prev) => [
372
+ ...prev,
373
+ {
374
+ timestamp: new Date().toISOString(),
375
+ sender: 'system',
376
+ content: 'Connection lost. Any active tunnels will be closed.',
377
+ },
378
+ ]);
379
+ // Close tunnel best-effort
380
+ void tunnelManager.closeTunnel().then(() => setActiveTunnel(null));
381
+ });
217
382
  return () => {
218
383
  wsClient.removeAllListeners();
219
384
  };
220
- }, [wsClient, taskId, apiClient, permissionManager, exit]);
221
- const endSession = async (reason = 'Customer ended session') => {
222
- if (endingRef.current)
385
+ }, [wsClient, taskId, apiClient, permissionManager, exit, tunnelManager, shareTunnel]);
386
+ const processReturnKey = () => {
387
+ if (!inputBuffer.trim()) {
223
388
  return;
224
- endingRef.current = true;
225
- setMessages((prev) => [
226
- ...prev,
227
- {
228
- timestamp: new Date().toISOString(),
229
- sender: 'system',
230
- content: 'Ending session...',
231
- },
232
- ]);
233
- try {
234
- wsClient.send({
235
- type: 'end_session',
236
- timestamp: new Date().toISOString(),
237
- payload: { reason, endedBy: 'customer' },
238
- });
239
389
  }
240
- catch (error) {
241
- // ignore send errors on shutdown
242
- }
243
- try {
244
- await apiClient.endSession(taskId);
245
- }
246
- catch (error) {
247
- // best-effort; still proceed to exit
248
- }
249
- try {
250
- wsClient.disconnect();
251
- }
252
- catch (error) {
253
- // ignore
254
- }
255
- setTimeout(() => exit(), 200);
256
- };
257
- useInput((input, key) => {
258
- if (key.return && inputBuffer.trim()) {
259
- const message = inputBuffer.trim();
260
- // Shell mode handling (local only)
261
- if (mode === 'shell') {
262
- if (message === '/resume' || message === '/session') {
263
- setMode('session');
264
- setMessages((prev) => [
265
- ...prev,
266
- {
267
- timestamp: new Date().toISOString(),
268
- sender: 'system',
269
- content: 'Back to session (messages will be shared with engineer).',
270
- },
271
- ]);
272
- setInputBuffer('');
273
- return;
274
- }
275
- if (message === '/end') {
276
- endSession();
277
- setInputBuffer('');
278
- return;
279
- }
280
- if (message === '/shell') {
281
- setMessages((prev) => [
282
- ...prev,
283
- {
284
- timestamp: new Date().toISOString(),
285
- sender: 'system',
286
- content: 'Already in shell mode. Type /resume to return to session.',
287
- },
288
- ]);
289
- setInputBuffer('');
290
- return;
291
- }
292
- if (message === '/help' || message === '/?') {
293
- setMessages((prev) => [
294
- ...prev,
295
- ...COMMANDS.map((c) => ({
296
- timestamp: new Date().toISOString(),
297
- sender: 'system',
298
- content: `${c.cmd} — ${c.description}`,
299
- })),
300
- {
301
- timestamp: new Date().toISOString(),
302
- sender: 'system',
303
- content: '/resume — return to session mode',
304
- },
305
- ]);
306
- setInputBuffer('');
307
- return;
308
- }
309
- if (message.startsWith('/')) {
310
- setMessages((prev) => [
311
- ...prev,
312
- {
313
- timestamp: new Date().toISOString(),
314
- sender: 'system',
315
- content: `Unknown command in shell: ${message}`,
316
- },
317
- ]);
318
- setInputBuffer('');
319
- return;
320
- }
321
- // Execute local command
322
- (async () => {
323
- try {
324
- const { stdout, stderr } = await execAsync(message, { cwd: process.cwd() });
325
- setMessages((prev) => [
326
- ...prev,
327
- {
328
- timestamp: new Date().toISOString(),
329
- sender: 'system',
330
- content: `$ ${message}\n${stdout || ''}${stderr ? `\n${stderr}` : ''}`.trim(),
331
- },
332
- ]);
333
- }
334
- catch (error) {
335
- setMessages((prev) => [
336
- ...prev,
337
- {
338
- timestamp: new Date().toISOString(),
339
- sender: 'system',
340
- content: `$ ${message}\n${error.stdout || ''}${error.stderr || error.message || 'Command failed'}`,
341
- },
342
- ]);
343
- }
344
- })();
390
+ const message = inputBuffer.trim();
391
+ // Shell mode handling
392
+ if (mode === 'shell') {
393
+ if (message === '/resume' || message === '/session') {
394
+ setMode('session');
395
+ setMessages((prev) => [
396
+ ...prev,
397
+ {
398
+ timestamp: new Date().toISOString(),
399
+ sender: 'system',
400
+ content: 'Back to session (messages will be shared with engineer).',
401
+ },
402
+ ]);
345
403
  setInputBuffer('');
346
404
  return;
347
405
  }
348
- // Session mode handling
349
406
  if (message === '/end') {
350
407
  endSession();
351
408
  setInputBuffer('');
352
409
  return;
353
410
  }
354
- if (message === '/help' || message === '/?') {
411
+ if (message === '/shell') {
355
412
  setMessages((prev) => [
356
413
  ...prev,
357
- ...COMMANDS.map((c) => ({
414
+ {
358
415
  timestamp: new Date().toISOString(),
359
416
  sender: 'system',
360
- content: `${c.cmd} ${c.description}`,
361
- })),
417
+ content: 'Already in shell mode. Type /resume to return to session.',
418
+ },
419
+ ]);
420
+ setInputBuffer('');
421
+ return;
422
+ }
423
+ if (message === '/help' || message === '/?') {
424
+ setMessages((prev) => [
425
+ ...prev,
362
426
  {
363
427
  timestamp: new Date().toISOString(),
364
428
  sender: 'system',
365
- content: '/shell — switch to local shell (not shared)',
429
+ content: 'Available commands:',
366
430
  },
431
+ ...COMMANDS.map((c) => ({
432
+ timestamp: new Date().toISOString(),
433
+ sender: 'system',
434
+ content: `${c.cmd} — ${c.description}`,
435
+ })),
367
436
  ]);
368
437
  setInputBuffer('');
369
438
  return;
370
439
  }
371
- if (message === '/shell') {
372
- setMode('shell');
440
+ if (message.startsWith('/')) {
373
441
  setMessages((prev) => [
374
442
  ...prev,
375
443
  {
376
444
  timestamp: new Date().toISOString(),
377
445
  sender: 'system',
378
- content: 'Switched to local shell mode (commands are NOT shared with engineer). Type /resume to return.',
446
+ content: `Unknown command in shell: ${message}`,
379
447
  },
380
448
  ]);
381
449
  setInputBuffer('');
382
450
  return;
383
451
  }
384
- wsClient.send({
385
- type: 'customer_message',
386
- timestamp: new Date().toISOString(),
387
- payload: { message },
388
- });
452
+ // Execute local command
453
+ (async () => {
454
+ try {
455
+ const { stdout, stderr } = await execAsync(message, { cwd: process.cwd() });
456
+ setMessages((prev) => [
457
+ ...prev,
458
+ {
459
+ timestamp: new Date().toISOString(),
460
+ sender: 'system',
461
+ content: `$ ${message}\n${stdout || ''}${stderr ? `\n${stderr}` : ''}`.trim(),
462
+ },
463
+ ]);
464
+ }
465
+ catch (error) {
466
+ setMessages((prev) => [
467
+ ...prev,
468
+ {
469
+ timestamp: new Date().toISOString(),
470
+ sender: 'system',
471
+ content: `$ ${message}\n${error.stdout || ''}${error.stderr || error.message || 'Command failed'}`,
472
+ },
473
+ ]);
474
+ }
475
+ })();
476
+ setInputBuffer('');
477
+ return;
478
+ }
479
+ // Session mode handling
480
+ if (message === '/end') {
481
+ endSession();
482
+ setInputBuffer('');
483
+ return;
484
+ }
485
+ if (message === '/hybrid') {
389
486
  setMessages((prev) => [
390
487
  ...prev,
391
488
  {
392
489
  timestamp: new Date().toISOString(),
393
- sender: 'customer',
394
- content: message,
490
+ sender: 'system',
491
+ content: 'Switched to Hybrid mode (AI → Human fallback). This is the default mode.',
395
492
  },
396
493
  ]);
397
494
  setInputBuffer('');
495
+ return;
398
496
  }
399
- else if (key.backspace || key.delete) {
400
- setInputBuffer((prev) => prev.slice(0, -1));
497
+ if (message === '/ai') {
498
+ setMessages((prev) => [
499
+ ...prev,
500
+ {
501
+ timestamp: new Date().toISOString(),
502
+ sender: 'system',
503
+ content: 'Switched to AI-only mode. Only AI assistance will be used.',
504
+ },
505
+ ]);
506
+ setInputBuffer('');
507
+ return;
508
+ }
509
+ if (message === '/human') {
510
+ setMessages((prev) => [
511
+ ...prev,
512
+ {
513
+ timestamp: new Date().toISOString(),
514
+ sender: 'system',
515
+ content: 'Requesting human engineer... (2 credits/min)',
516
+ },
517
+ ]);
518
+ setInputBuffer('');
519
+ return;
520
+ }
521
+ if (message === '/help' || message === '/?') {
522
+ setMessages((prev) => [
523
+ ...prev,
524
+ {
525
+ timestamp: new Date().toISOString(),
526
+ sender: 'system',
527
+ content: 'Available commands:',
528
+ },
529
+ ...COMMANDS.map((c) => ({
530
+ timestamp: new Date().toISOString(),
531
+ sender: 'system',
532
+ content: `${c.cmd} — ${c.description}`,
533
+ })),
534
+ ]);
535
+ setInputBuffer('');
536
+ return;
401
537
  }
402
- else if (!key.ctrl && !key.meta && input) {
403
- setInputBuffer((prev) => prev + input);
538
+ if (message === '/shell') {
539
+ setMode('shell');
540
+ setMessages((prev) => [
541
+ ...prev,
542
+ {
543
+ timestamp: new Date().toISOString(),
544
+ sender: 'system',
545
+ content: 'Switched to local shell mode (commands are NOT shared with engineer). Type /resume to return.',
546
+ },
547
+ ]);
548
+ setInputBuffer('');
549
+ return;
550
+ }
551
+ if (message.startsWith('/tunnel')) {
552
+ const parts = message.split(' ').filter(Boolean);
553
+ const port = Number(parts[1]);
554
+ if (!port || Number.isNaN(port)) {
555
+ setMessages((prev) => [
556
+ ...prev,
557
+ {
558
+ timestamp: new Date().toISOString(),
559
+ sender: 'system',
560
+ content: 'Usage: /tunnel <port>',
561
+ },
562
+ ]);
563
+ setInputBuffer('');
564
+ return;
565
+ }
566
+ void shareTunnel(port);
567
+ setInputBuffer('');
568
+ return;
569
+ }
570
+ if (message === '/close-tunnel') {
571
+ void closeActiveTunnel();
572
+ setInputBuffer('');
573
+ return;
574
+ }
575
+ wsClient.send({
576
+ type: 'customer_message',
577
+ timestamp: new Date().toISOString(),
578
+ payload: { message },
579
+ });
580
+ setMessages((prev) => [
581
+ ...prev,
582
+ {
583
+ timestamp: new Date().toISOString(),
584
+ sender: 'customer',
585
+ content: message,
586
+ },
587
+ ]);
588
+ setInputBuffer('');
589
+ };
590
+ const endSession = async (reason = 'Customer ended session') => {
591
+ if (endingRef.current)
592
+ return;
593
+ endingRef.current = true;
594
+ try {
595
+ wsClient.send({
596
+ type: 'end_session',
597
+ timestamp: new Date().toISOString(),
598
+ payload: { reason, endedBy: 'customer' },
599
+ });
600
+ }
601
+ catch (error) {
602
+ // ignore send errors on shutdown
603
+ }
604
+ try {
605
+ await apiClient.endSession(taskId);
606
+ }
607
+ catch (error) {
608
+ // best-effort; still proceed to exit
609
+ }
610
+ try {
611
+ await tunnelManager.closeTunnel();
612
+ setActiveTunnel(null);
613
+ }
614
+ catch (error) {
615
+ // ignore cleanup errors
616
+ }
617
+ try {
618
+ wsClient.disconnect();
619
+ }
620
+ catch (error) {
621
+ // ignore
622
+ }
623
+ setTimeout(() => exit(), 200);
624
+ };
625
+ useInput((input, key) => {
626
+ const now = Date.now();
627
+ // DEBUG: Log all inputs (remove after testing)
628
+ if (process.env.CODEVF_DEBUG) {
629
+ console.error(`[DEBUG] input="${input}" length=${input?.length} key.return=${key.return} key.sequence="${key.sequence}" isPasteMode=${isPasteMode}`);
630
+ }
631
+ // Handle bracketed paste mode sequences
632
+ if (key.sequence === '\x1b[200~') {
633
+ setIsPasteMode(true);
634
+ blockReturnsUntilRef.current = now + 500;
635
+ return;
636
+ }
637
+ if (key.sequence === '\x1b[201~') {
638
+ setIsPasteMode(false);
639
+ // Allow returns again after 100ms
640
+ blockReturnsUntilRef.current = now + 100;
641
+ return;
642
+ }
643
+ // If we receive ANY text input (not a control key), block returns for 100ms
644
+ // This catches paste operations even without bracketed paste mode
645
+ if (input && input.length > 0 && !key.return && !key.backspace && !key.delete) {
646
+ lastInputTimeRef.current = now;
647
+ blockReturnsUntilRef.current = now + 100;
648
+ // If multi-character input, definitely a paste
649
+ if (input.length > 1) {
650
+ setIsPasteMode(true);
651
+ blockReturnsUntilRef.current = now + 300;
652
+ }
653
+ // Strip newlines from input and add to buffer
654
+ const sanitized = input.replace(/[\r\n]+/g, ' ');
655
+ if (sanitized) {
656
+ setInputBuffer((prev) => prev + sanitized);
657
+ }
658
+ return;
659
+ }
660
+ // Handle return key
661
+ if (key.return) {
662
+ // If returns are blocked (recent text input), ignore this return
663
+ if (now < blockReturnsUntilRef.current) {
664
+ if (process.env.CODEVF_DEBUG) {
665
+ console.error(`[DEBUG] Return blocked for ${blockReturnsUntilRef.current - now}ms`);
666
+ }
667
+ setIsPasteMode(true);
668
+ // Extend block period
669
+ blockReturnsUntilRef.current = now + 100;
670
+ return;
671
+ }
672
+ // Returns are not blocked - this is a real Enter key press
673
+ setIsPasteMode(false);
674
+ processReturnKey();
675
+ return;
676
+ }
677
+ // Handle backspace/delete
678
+ if (key.backspace || key.delete) {
679
+ setInputBuffer((prev) => prev.slice(0, -1));
680
+ return;
404
681
  }
405
682
  }, { isActive: !isProcessingRequest });
406
683
  const formatTime = (timestamp) => {
@@ -408,19 +685,19 @@ export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, })
408
685
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
409
686
  };
410
687
  return (React.createElement(Box, { flexDirection: "column", height: "100%" },
411
- React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1 },
688
+ React.createElement(Box, { borderStyle: "round", borderColor: "magenta", paddingX: 1, marginBottom: 1 },
412
689
  React.createElement(Box, { flexDirection: "column" },
413
- React.createElement(Text, { bold: true, color: "cyan" }, "CodeVF Live Session"),
414
- engineerName && (React.createElement(Text, { color: "white" },
690
+ React.createElement(Text, { bold: true, color: "magenta", backgroundColor: "black" }, "CodeVF Engineer Session"),
691
+ engineerName && (React.createElement(Text, { color: "cyan" },
415
692
  "Engineer: ",
416
693
  engineerName,
417
- " ",
418
- engineerTitle && React.createElement(Text, { dimColor: true },
694
+ ' ',
695
+ engineerTitle && React.createElement(Text, { color: "gray" },
419
696
  "(",
420
697
  engineerTitle,
421
698
  ")"))),
422
- React.createElement(Text, { dimColor: true },
423
- "Billing: ",
699
+ React.createElement(Text, { color: "yellow" },
700
+ "\uD83D\uDCB0 Credits: ",
424
701
  creditsUsed,
425
702
  " credit",
426
703
  creditsUsed !== 1 ? 's' : '',
@@ -431,21 +708,25 @@ export const LiveSession = ({ taskId, wsClient, apiClient, permissionManager, })
431
708
  formatTime(msg.timestamp),
432
709
  "]"),
433
710
  React.createElement(Text, null, " "),
434
- msg.sender === 'engineer' && React.createElement(Text, { color: "green" },
435
- engineerName || 'Engineer',
436
- ":"),
437
- msg.sender === 'customer' && React.createElement(Text, { color: "blue" }, "You:"),
438
- msg.sender === 'system' && React.createElement(Text, { color: "yellow" }, "System:"),
439
- React.createElement(Text, null,
440
- " ",
441
- msg.content))))),
442
- React.createElement(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1 },
443
- React.createElement(Text, { color: "blue" }, mode === 'shell' ? 'Local> ' : 'You: '),
711
+ msg.sender === 'engineer' && (React.createElement(React.Fragment, null,
712
+ React.createElement(Text, { color: "magenta", bold: true, backgroundColor: "black" }, "Engineer:"),
713
+ React.createElement(Text, null, " "))),
714
+ msg.sender === 'customer' && (React.createElement(React.Fragment, null,
715
+ React.createElement(Text, { color: "green", bold: true, backgroundColor: "black" }, "\uD83D\uDC64 You:"),
716
+ React.createElement(Text, null, " "))),
717
+ msg.sender === 'system' && (React.createElement(React.Fragment, null,
718
+ React.createElement(Text, { color: "yellow", bold: true, backgroundColor: "blue" }, "\u2139\uFE0F System:"),
719
+ React.createElement(Text, null, " "))),
720
+ msg.sender === 'engineer' ? (React.createElement(Text, { color: "cyan", backgroundColor: "black" }, msg.content)) : (React.createElement(Text, null, msg.content)))))),
721
+ React.createElement(Box, { borderStyle: "round", borderColor: mode === 'shell' ? 'yellow' : 'magenta', paddingX: 1 },
722
+ React.createElement(Text, { color: mode === 'shell' ? 'yellow' : 'magenta', bold: true }, mode === 'shell' ? '🐚 Local> ' : '💬 Chat: '),
444
723
  React.createElement(Text, null, inputBuffer),
445
724
  React.createElement(Text, { color: "gray" }, "\u258C")),
446
725
  React.createElement(Box, { marginTop: 1 },
447
- React.createElement(Text, { dimColor: true }, mode === 'shell'
448
- ? 'Shell mode (not shared). /resume to return, /? for commands.'
449
- : 'Type `/?` to see available commands (e.g., /end, /shell)'))));
726
+ React.createElement(Text, { color: isPasteMode ? 'yellow' : 'cyan' }, isPasteMode
727
+ ? '📋 Paste mode active - Press Enter when done pasting'
728
+ : mode === 'shell'
729
+ ? '🔒 Shell mode (not shared). /resume to return, /? for commands.'
730
+ : '💡 Quick: /auto /ai /human /shell /tunnel /? /end'))));
450
731
  };
451
732
  //# sourceMappingURL=LiveSession.js.map