@qduc/term2 0.1.2 → 0.1.4

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 (216) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +5 -0
  3. package/dist/agent.js.map +1 -1
  4. package/dist/app.d.ts.map +1 -1
  5. package/dist/app.js +3 -1
  6. package/dist/app.js.map +1 -1
  7. package/dist/cli.js +2 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/components/ApprovalPrompt.d.ts.map +1 -1
  10. package/dist/components/ApprovalPrompt.js +20 -0
  11. package/dist/components/ApprovalPrompt.js.map +1 -1
  12. package/dist/components/ChatMessage.js +1 -1
  13. package/dist/components/ChatMessage.js.map +1 -1
  14. package/dist/components/InputBox.d.ts.map +1 -1
  15. package/dist/components/InputBox.js +3 -8
  16. package/dist/components/InputBox.js.map +1 -1
  17. package/dist/components/MarkdownRenderer.js +1 -1
  18. package/dist/components/MarkdownRenderer.js.map +1 -1
  19. package/dist/components/ModelSelectionMenu.error-tabs.test.d.ts +2 -0
  20. package/dist/components/ModelSelectionMenu.error-tabs.test.d.ts.map +1 -0
  21. package/dist/components/ModelSelectionMenu.error-tabs.test.js +18 -0
  22. package/dist/components/ModelSelectionMenu.error-tabs.test.js.map +1 -0
  23. package/dist/components/SettingsSelectionMenu.js +1 -1
  24. package/dist/components/SettingsSelectionMenu.js.map +1 -1
  25. package/dist/components/SlashCommandMenu.js +2 -2
  26. package/dist/components/SlashCommandMenu.js.map +1 -1
  27. package/dist/hooks/use-conversation.d.ts.map +1 -1
  28. package/dist/hooks/use-conversation.js +53 -508
  29. package/dist/hooks/use-conversation.js.map +1 -1
  30. package/dist/hooks/use-model-selection.d.ts.map +1 -1
  31. package/dist/hooks/use-model-selection.js +7 -3
  32. package/dist/hooks/use-model-selection.js.map +1 -1
  33. package/dist/hooks/use-settings-completion.d.ts.map +1 -1
  34. package/dist/hooks/use-settings-completion.js +5 -0
  35. package/dist/hooks/use-settings-completion.js.map +1 -1
  36. package/dist/hooks/use-settings-completion.test.js +6 -0
  37. package/dist/hooks/use-settings-completion.test.js.map +1 -1
  38. package/dist/hooks/use-settings-value-completion.d.ts.map +1 -1
  39. package/dist/hooks/use-settings-value-completion.js +1 -0
  40. package/dist/hooks/use-settings-value-completion.js.map +1 -1
  41. package/dist/lib/editor-impl.test.d.ts +2 -0
  42. package/dist/lib/editor-impl.test.d.ts.map +1 -0
  43. package/dist/lib/editor-impl.test.js +188 -0
  44. package/dist/lib/editor-impl.test.js.map +1 -0
  45. package/dist/lib/openai-agent-client.d.ts.map +1 -1
  46. package/dist/lib/openai-agent-client.js +35 -15
  47. package/dist/lib/openai-agent-client.js.map +1 -1
  48. package/dist/lib/openai-agent-client.public-methods.test.d.ts +2 -0
  49. package/dist/lib/openai-agent-client.public-methods.test.d.ts.map +1 -0
  50. package/dist/lib/openai-agent-client.public-methods.test.js +188 -0
  51. package/dist/lib/openai-agent-client.public-methods.test.js.map +1 -0
  52. package/dist/lib/tool-invoke.test.js +1 -1
  53. package/dist/lib/tool-invoke.test.js.map +1 -1
  54. package/dist/providers/github-copilot/converters.d.ts +45 -0
  55. package/dist/providers/github-copilot/converters.d.ts.map +1 -0
  56. package/dist/providers/github-copilot/converters.js +118 -0
  57. package/dist/providers/github-copilot/converters.js.map +1 -0
  58. package/dist/providers/github-copilot/converters.test.d.ts +2 -0
  59. package/dist/providers/github-copilot/converters.test.d.ts.map +1 -0
  60. package/dist/providers/github-copilot/converters.test.js +162 -0
  61. package/dist/providers/github-copilot/converters.test.js.map +1 -0
  62. package/dist/providers/github-copilot/github-copilot.provider.d.ts +2 -0
  63. package/dist/providers/github-copilot/github-copilot.provider.d.ts.map +1 -0
  64. package/dist/providers/github-copilot/github-copilot.provider.js +75 -0
  65. package/dist/providers/github-copilot/github-copilot.provider.js.map +1 -0
  66. package/dist/providers/github-copilot/github-copilot.provider.test.d.ts +2 -0
  67. package/dist/providers/github-copilot/github-copilot.provider.test.d.ts.map +1 -0
  68. package/dist/providers/github-copilot/github-copilot.provider.test.js +26 -0
  69. package/dist/providers/github-copilot/github-copilot.provider.test.js.map +1 -0
  70. package/dist/providers/github-copilot/index.d.ts +4 -0
  71. package/dist/providers/github-copilot/index.d.ts.map +1 -0
  72. package/dist/providers/github-copilot/index.js +4 -0
  73. package/dist/providers/github-copilot/index.js.map +1 -0
  74. package/dist/providers/github-copilot/model-direct.d.ts +34 -0
  75. package/dist/providers/github-copilot/model-direct.d.ts.map +1 -0
  76. package/dist/providers/github-copilot/model-direct.js +443 -0
  77. package/dist/providers/github-copilot/model-direct.js.map +1 -0
  78. package/dist/providers/github-copilot/model.d.ts +24 -0
  79. package/dist/providers/github-copilot/model.d.ts.map +1 -0
  80. package/dist/providers/github-copilot/model.delta.test.d.ts +2 -0
  81. package/dist/providers/github-copilot/model.delta.test.d.ts.map +1 -0
  82. package/dist/providers/github-copilot/model.delta.test.js +15 -0
  83. package/dist/providers/github-copilot/model.delta.test.js.map +1 -0
  84. package/dist/providers/github-copilot/model.js +581 -0
  85. package/dist/providers/github-copilot/model.js.map +1 -0
  86. package/dist/providers/github-copilot/provider.d.ts +20 -0
  87. package/dist/providers/github-copilot/provider.d.ts.map +1 -0
  88. package/dist/providers/github-copilot/provider.js +30 -0
  89. package/dist/providers/github-copilot/provider.js.map +1 -0
  90. package/dist/providers/github-copilot/provider.test.d.ts +2 -0
  91. package/dist/providers/github-copilot/provider.test.d.ts.map +1 -0
  92. package/dist/providers/github-copilot/provider.test.js +52 -0
  93. package/dist/providers/github-copilot/provider.test.js.map +1 -0
  94. package/dist/providers/github-copilot/utils.d.ts +20 -0
  95. package/dist/providers/github-copilot/utils.d.ts.map +1 -0
  96. package/dist/providers/github-copilot/utils.js +142 -0
  97. package/dist/providers/github-copilot/utils.js.map +1 -0
  98. package/dist/providers/github-copilot/utils.test.d.ts +2 -0
  99. package/dist/providers/github-copilot/utils.test.d.ts.map +1 -0
  100. package/dist/providers/github-copilot/utils.test.js +21 -0
  101. package/dist/providers/github-copilot/utils.test.js.map +1 -0
  102. package/dist/providers/openai-compatible/model.d.ts.map +1 -1
  103. package/dist/providers/openai-compatible/model.js +13 -2
  104. package/dist/providers/openai-compatible/model.js.map +1 -1
  105. package/dist/providers/openai-compatible/reasoning-content.test.js +2 -2
  106. package/dist/providers/openai-compatible/reasoning-content.test.js.map +1 -1
  107. package/dist/providers/openrouter/converters.d.ts.map +1 -1
  108. package/dist/providers/openrouter/converters.js +64 -46
  109. package/dist/providers/openrouter/converters.js.map +1 -1
  110. package/dist/providers/openrouter/converters.test.js +13 -12
  111. package/dist/providers/openrouter/converters.test.js.map +1 -1
  112. package/dist/providers/openrouter/merge-messages.test.d.ts +2 -0
  113. package/dist/providers/openrouter/merge-messages.test.d.ts.map +1 -0
  114. package/dist/providers/openrouter/merge-messages.test.js +83 -0
  115. package/dist/providers/openrouter/merge-messages.test.js.map +1 -0
  116. package/dist/providers/openrouter/reasoning-content.test.js +2 -2
  117. package/dist/providers/openrouter/reasoning-content.test.js.map +1 -1
  118. package/dist/providers/openrouter.test.js +30 -21
  119. package/dist/providers/openrouter.test.js.map +1 -1
  120. package/dist/reproduce_issue.test.d.ts +2 -0
  121. package/dist/reproduce_issue.test.d.ts.map +1 -0
  122. package/dist/reproduce_issue.test.js +31 -0
  123. package/dist/reproduce_issue.test.js.map +1 -0
  124. package/dist/services/conversation-store.test.js +12 -11
  125. package/dist/services/conversation-store.test.js.map +1 -1
  126. package/dist/services/settings-service.d.ts +12 -3
  127. package/dist/services/settings-service.d.ts.map +1 -1
  128. package/dist/services/settings-service.js +26 -0
  129. package/dist/services/settings-service.js.map +1 -1
  130. package/dist/tools/ask-mentor.d.ts +1 -1
  131. package/dist/tools/ask-mentor.d.ts.map +1 -1
  132. package/dist/tools/ask-mentor.js +1 -0
  133. package/dist/tools/ask-mentor.js.map +1 -1
  134. package/dist/tools/edit-healing.d.ts +24 -0
  135. package/dist/tools/edit-healing.d.ts.map +1 -0
  136. package/dist/tools/edit-healing.js +230 -0
  137. package/dist/tools/edit-healing.js.map +1 -0
  138. package/dist/tools/edit-healing.test.d.ts +2 -0
  139. package/dist/tools/edit-healing.test.d.ts.map +1 -0
  140. package/dist/tools/edit-healing.test.js +34 -0
  141. package/dist/tools/edit-healing.test.js.map +1 -0
  142. package/dist/tools/find-files.d.ts +2 -2
  143. package/dist/tools/find-files.d.ts.map +1 -1
  144. package/dist/tools/find-files.js +2 -0
  145. package/dist/tools/find-files.js.map +1 -1
  146. package/dist/tools/grep.d.ts +1 -1
  147. package/dist/tools/grep.d.ts.map +1 -1
  148. package/dist/tools/grep.js +1 -0
  149. package/dist/tools/grep.js.map +1 -1
  150. package/dist/tools/read-file.d.ts.map +1 -1
  151. package/dist/tools/read-file.js +10 -9
  152. package/dist/tools/read-file.js.map +1 -1
  153. package/dist/tools/read-file.test.js +24 -19
  154. package/dist/tools/read-file.test.js.map +1 -1
  155. package/dist/tools/search-replace.d.ts +3 -1
  156. package/dist/tools/search-replace.d.ts.map +1 -1
  157. package/dist/tools/search-replace.js +331 -19
  158. package/dist/tools/search-replace.js.map +1 -1
  159. package/dist/tools/search-replace.test.js +241 -2
  160. package/dist/tools/search-replace.test.js.map +1 -1
  161. package/dist/tools/search.d.ts +4 -4
  162. package/dist/tools/search.d.ts.map +1 -1
  163. package/dist/tools/search.js +4 -0
  164. package/dist/tools/search.js.map +1 -1
  165. package/dist/tools/shell.d.ts +2 -2
  166. package/dist/tools/shell.d.ts.map +1 -1
  167. package/dist/tools/shell.js +2 -0
  168. package/dist/tools/shell.js.map +1 -1
  169. package/dist/tools/types.d.ts +2 -2
  170. package/dist/tools/types.d.ts.map +1 -1
  171. package/dist/tools/web-fetch.d.ts +20 -0
  172. package/dist/tools/web-fetch.d.ts.map +1 -0
  173. package/dist/tools/web-fetch.js +300 -0
  174. package/dist/tools/web-fetch.js.map +1 -0
  175. package/dist/tools/web-fetch.test.d.ts +2 -0
  176. package/dist/tools/web-fetch.test.d.ts.map +1 -0
  177. package/dist/tools/web-fetch.test.js +94 -0
  178. package/dist/tools/web-fetch.test.js.map +1 -0
  179. package/dist/utils/command-safety/index.d.ts +2 -2
  180. package/dist/utils/command-safety/index.d.ts.map +1 -1
  181. package/dist/utils/command-safety/index.js +7 -6
  182. package/dist/utils/command-safety/index.js.map +1 -1
  183. package/dist/utils/command-safety.find.test.js +19 -21
  184. package/dist/utils/command-safety.find.test.js.map +1 -1
  185. package/dist/utils/conversation-event-handler.d.ts +63 -0
  186. package/dist/utils/conversation-event-handler.d.ts.map +1 -0
  187. package/dist/utils/conversation-event-handler.js +132 -0
  188. package/dist/utils/conversation-event-handler.js.map +1 -0
  189. package/dist/utils/conversation-event-handler.test.d.ts +2 -0
  190. package/dist/utils/conversation-event-handler.test.d.ts.map +1 -0
  191. package/dist/utils/conversation-event-handler.test.js +281 -0
  192. package/dist/utils/conversation-event-handler.test.js.map +1 -0
  193. package/dist/utils/conversation-utils.d.ts +41 -0
  194. package/dist/utils/conversation-utils.d.ts.map +1 -0
  195. package/dist/utils/conversation-utils.js +109 -0
  196. package/dist/utils/conversation-utils.js.map +1 -0
  197. package/dist/utils/conversation-utils.test.d.ts +2 -0
  198. package/dist/utils/conversation-utils.test.d.ts.map +1 -0
  199. package/dist/utils/conversation-utils.test.js +190 -0
  200. package/dist/utils/conversation-utils.test.js.map +1 -0
  201. package/dist/utils/ink-render-options.d.ts +9 -0
  202. package/dist/utils/ink-render-options.d.ts.map +1 -0
  203. package/dist/utils/ink-render-options.js +8 -0
  204. package/dist/utils/ink-render-options.js.map +1 -0
  205. package/dist/utils/message-utils.d.ts +17 -0
  206. package/dist/utils/message-utils.d.ts.map +1 -0
  207. package/dist/utils/message-utils.js +52 -0
  208. package/dist/utils/message-utils.js.map +1 -0
  209. package/dist/utils/message-utils.test.d.ts +2 -0
  210. package/dist/utils/message-utils.test.d.ts.map +1 -0
  211. package/dist/utils/message-utils.test.js +48 -0
  212. package/dist/utils/message-utils.test.js.map +1 -0
  213. package/dist/utils/settings-command.d.ts.map +1 -1
  214. package/dist/utils/settings-command.js +13 -4
  215. package/dist/utils/settings-command.js.map +1 -1
  216. package/package.json +10 -4
@@ -2,6 +2,8 @@ import { useCallback, useRef, useState } from 'react';
2
2
  import { isAbortLikeError } from '../utils/error-helpers.js';
3
3
  import { createStreamingUpdateCoordinator } from '../utils/streaming-updater.js';
4
4
  import { appendMessagesCapped } from '../utils/message-buffer.js';
5
+ import { createStreamingState, enhanceApiKeyError, isMaxTurnsError, } from '../utils/conversation-utils.js';
6
+ import { createConversationEventHandler } from '../utils/conversation-event-handler.js';
5
7
  const LIVE_RESPONSE_THROTTLE_MS = 150;
6
8
  const REASONING_RESPONSE_THROTTLE_MS = 200;
7
9
  const MAX_MESSAGE_COUNT = 300;
@@ -180,16 +182,12 @@ export const useConversation = ({ conversationService, loggingService, }) => {
180
182
  text: '',
181
183
  });
182
184
  const liveResponseUpdater = createLiveResponseUpdater(liveMessageId);
183
- // Track accumulated text so we can flush it before command messages
184
- let accumulatedText = '';
185
- let accumulatedReasoningText = '';
186
- let flushedReasoningLength = 0; // Track how much reasoning has been flushed
187
- let textWasFlushed = false;
188
- let currentReasoningMessageId = null; // Track current reasoning message ID
185
+ // Create streaming state object for this message send
186
+ const streamingState = createStreamingState();
189
187
  const reasoningUpdater = createStreamingUpdateCoordinator((newReasoningText) => {
190
188
  setMessages(prev => {
191
- if (currentReasoningMessageId !== null) {
192
- const index = prev.findIndex(msg => msg.id === currentReasoningMessageId);
189
+ if (streamingState.currentReasoningMessageId !== null) {
190
+ const index = prev.findIndex(msg => msg.id === streamingState.currentReasoningMessageId);
193
191
  if (index === -1)
194
192
  return prev;
195
193
  const current = prev[index];
@@ -201,7 +199,7 @@ export const useConversation = ({ conversationService, loggingService, }) => {
201
199
  return trimMessages(next);
202
200
  }
203
201
  const newId = Date.now();
204
- currentReasoningMessageId = newId;
202
+ streamingState.currentReasoningMessageId = newId;
205
203
  return trimMessages([
206
204
  ...prev,
207
205
  {
@@ -212,175 +210,21 @@ export const useConversation = ({ conversationService, loggingService, }) => {
212
210
  ]);
213
211
  });
214
212
  }, REASONING_RESPONSE_THROTTLE_MS);
215
- // Create event logger with deduplication for this message send
216
- // const {logDeduplicated, flush: flushLog} = createEventLogger();
217
- const applyConversationEvent = (event) => {
218
- switch (event.type) {
219
- case 'text_delta': {
220
- // logDeduplicated('text_delta');
221
- accumulatedText += event.delta;
222
- liveResponseUpdater.push(accumulatedText);
223
- return;
224
- }
225
- case 'reasoning_delta': {
226
- // logDeduplicated('reasoning_delta');
227
- const fullReasoningText = event.fullText ?? '';
228
- // Only show reasoning text after what was already flushed
229
- const newReasoningText = fullReasoningText.slice(flushedReasoningLength);
230
- accumulatedReasoningText = newReasoningText;
231
- if (!newReasoningText.trim())
232
- return;
233
- reasoningUpdater.push(newReasoningText);
234
- return;
235
- }
236
- case 'tool_started': {
237
- // Flush reasoning state
238
- if (accumulatedReasoningText.trim()) {
239
- reasoningUpdater.flush();
240
- flushedReasoningLength += accumulatedReasoningText.length;
241
- accumulatedReasoningText = '';
242
- currentReasoningMessageId = null;
243
- }
244
- // Flush any accumulated text before showing the tool call
245
- if (accumulatedText.trim()) {
246
- const textMessage = {
247
- id: Date.now() + 1,
248
- sender: 'bot',
249
- text: accumulatedText,
250
- };
251
- appendMessages([textMessage]);
252
- accumulatedText = '';
253
- textWasFlushed = true;
254
- liveResponseUpdater.cancel();
255
- setLiveResponse(null);
256
- }
257
- // Emit a "pending" command message when tool starts running
258
- // This provides immediate UI feedback before output is available
259
- const { toolCallId, toolName, arguments: rawArgs } = event;
260
- // tool_started.arguments may be either an object or a JSON string
261
- // depending on provider. Normalize so we can render params.
262
- const args = (() => {
263
- if (typeof rawArgs !== 'string') {
264
- return rawArgs;
265
- }
266
- const trimmed = rawArgs.trim();
267
- if (!trimmed) {
268
- return rawArgs;
269
- }
270
- try {
271
- return JSON.parse(trimmed);
272
- }
273
- catch {
274
- return rawArgs;
275
- }
276
- })();
277
- // Create a command string from the tool info
278
- const command = (() => {
279
- if (toolName === 'shell') {
280
- const cmd = args?.command ?? args?.commands;
281
- if (typeof cmd === 'string' && cmd.trim()) {
282
- return cmd;
283
- }
284
- if (Array.isArray(cmd) && cmd.length > 0) {
285
- return cmd.join('\n');
286
- }
287
- }
288
- if (toolName === 'grep' && args?.pattern) {
289
- return `grep "${args.pattern}" ${args.path ?? '.'}`;
290
- }
291
- if (toolName === 'search_replace') {
292
- return `search_replace "${args.search_content ?? ''}" → "${args.replace_content ?? ''}" ${args.path ?? ''}`;
293
- }
294
- if (toolName === 'apply_patch') {
295
- return `apply_patch ${args?.type ?? 'unknown'} ${args?.path ?? ''}`;
296
- }
297
- if (toolName === 'ask_mentor') {
298
- return `ask_mentor: ${args?.question ?? ''}`;
299
- }
300
- return `${toolName ?? 'unknown_tool'}`;
301
- })();
302
- const pendingMessage = {
303
- id: toolCallId ?? String(Date.now()),
304
- sender: 'command',
305
- status: 'running',
306
- command,
307
- output: '',
308
- callId: toolCallId,
309
- toolName,
310
- toolArgs: args,
311
- };
312
- appendMessages([pendingMessage]);
313
- return;
314
- }
315
- case 'command_message': {
316
- // logDeduplicated('command_message');
317
- const cmdMsg = event.message;
318
- const annotated = annotateCommandMessage(cmdMsg);
319
- // Before adding command message, flush reasoning and text separately
320
- // This preserves the order: reasoning -> command -> response text
321
- const messagesToAdd = [];
322
- if (accumulatedReasoningText.trim()) {
323
- reasoningUpdater.flush();
324
- // Reasoning is already in messages via stream updates.
325
- // We just need to track what we've "flushed" (sealed) so next reasoning chunks start fresh.
326
- flushedReasoningLength +=
327
- accumulatedReasoningText.length;
328
- accumulatedReasoningText = '';
329
- currentReasoningMessageId = null; // Reset for potential post-command reasoning
330
- }
331
- if (accumulatedText.trim()) {
332
- const textMessage = {
333
- id: Date.now() + 1,
334
- sender: 'bot',
335
- text: accumulatedText,
336
- };
337
- messagesToAdd.push(textMessage);
338
- accumulatedText = '';
339
- textWasFlushed = true;
340
- }
341
- if (messagesToAdd.length > 0) {
342
- appendMessages(messagesToAdd);
343
- // Clear live response since we've committed the text
344
- liveResponseUpdater.cancel();
345
- setLiveResponse(null);
346
- }
347
- // Replace pending message with completed one, or add new if not found
348
- setMessages(prev => {
349
- // Try to find the pending message by callId
350
- const pendingIndex = annotated.callId
351
- ? prev.findIndex(msg => msg.sender === 'command' &&
352
- msg.callId === annotated.callId &&
353
- msg.status === 'running')
354
- : -1;
355
- if (pendingIndex !== -1) {
356
- // Replace the pending message with the completed one
357
- const next = [...prev];
358
- next[pendingIndex] = annotated;
359
- return trimMessages(next);
360
- }
361
- // If no pending message found, append the completed one
362
- return trimMessages([...prev, annotated]);
363
- });
364
- return;
365
- }
366
- case 'retry': {
367
- const systemMessage = {
368
- id: Date.now(),
369
- sender: 'system',
370
- text: `Tool hallucination detected (${event.toolName}). Retrying... (Attempt ${event.attempt}/${event.maxRetries})`,
371
- };
372
- setMessages(prev => [...prev, systemMessage]);
373
- return;
374
- }
375
- default:
376
- return;
377
- }
378
- };
213
+ // Create event handler using extracted factory
214
+ const applyConversationEvent = createConversationEventHandler({
215
+ liveResponseUpdater,
216
+ reasoningUpdater,
217
+ appendMessages,
218
+ setMessages,
219
+ setLiveResponse,
220
+ trimMessages,
221
+ annotateCommandMessage,
222
+ }, streamingState);
379
223
  try {
380
224
  const result = await conversationService.sendMessage(value, {
381
225
  onEvent: applyConversationEvent,
382
226
  });
383
- applyServiceResult(result, accumulatedText, accumulatedReasoningText, textWasFlushed);
227
+ applyServiceResult(result, streamingState.accumulatedText, streamingState.accumulatedReasoningText, streamingState.textWasFlushed);
384
228
  }
385
229
  catch (error) {
386
230
  loggingService.error('Error in sendUserMessage', {
@@ -393,19 +237,9 @@ export const useConversation = ({ conversationService, loggingService, }) => {
393
237
  // The finally block will handle cleanup
394
238
  return;
395
239
  }
396
- let errorMessage = error instanceof Error ? error.message : String(error);
397
- // Enhance error messages for common issues
398
- if (errorMessage.includes('OPENAI_API_KEY') ||
399
- (errorMessage.includes('401') &&
400
- errorMessage.toLowerCase().includes('unauthorized'))) {
401
- errorMessage =
402
- 'OpenAI API key is not configured or invalid. Please set the OPENAI_API_KEY environment variable. ' +
403
- 'Get your API key from: https://platform.openai.com/api-keys';
404
- }
405
- // Check if this is a max turns exceeded error
406
- const isMaxTurnsError = errorMessage.includes('Max turns') &&
407
- errorMessage.includes('exceeded');
408
- if (isMaxTurnsError) {
240
+ const rawErrorMessage = error instanceof Error ? error.message : String(error);
241
+ const errorMessage = enhanceApiKeyError(rawErrorMessage);
242
+ if (isMaxTurnsError(errorMessage)) {
409
243
  // Create an approval prompt for max turns continuation
410
244
  setPendingApproval({
411
245
  agentName: 'System',
@@ -470,16 +304,12 @@ export const useConversation = ({ conversationService, loggingService, }) => {
470
304
  text: '',
471
305
  });
472
306
  const liveResponseUpdater = createLiveResponseUpdater(liveMessageId);
473
- // Track accumulated text so we can flush it before command messages
474
- let accumulatedText = '';
475
- let accumulatedReasoningText = '';
476
- let flushedReasoningLength = 0;
477
- let textWasFlushed = false;
478
- let currentReasoningMessageId = null;
307
+ // Create streaming state object for max turns continuation
308
+ const streamingState = createStreamingState();
479
309
  const reasoningUpdater = createStreamingUpdateCoordinator((newReasoningText) => {
480
310
  setMessages(prev => {
481
- if (currentReasoningMessageId !== null) {
482
- const index = prev.findIndex(msg => msg.id === currentReasoningMessageId);
311
+ if (streamingState.currentReasoningMessageId !== null) {
312
+ const index = prev.findIndex(msg => msg.id === streamingState.currentReasoningMessageId);
483
313
  if (index === -1)
484
314
  return prev;
485
315
  const current = prev[index];
@@ -494,7 +324,7 @@ export const useConversation = ({ conversationService, loggingService, }) => {
494
324
  return trimMessages(next);
495
325
  }
496
326
  const newId = Date.now();
497
- currentReasoningMessageId = newId;
327
+ streamingState.currentReasoningMessageId = newId;
498
328
  return trimMessages([
499
329
  ...prev,
500
330
  {
@@ -505,162 +335,23 @@ export const useConversation = ({ conversationService, loggingService, }) => {
505
335
  ]);
506
336
  });
507
337
  }, REASONING_RESPONSE_THROTTLE_MS);
508
- // const {logDeduplicated, flush: flushLog} = createEventLogger();
509
- const applyConversationEvent = (event) => {
510
- switch (event.type) {
511
- case 'text_delta': {
512
- // logDeduplicated('text_delta');
513
- accumulatedText += event.delta;
514
- liveResponseUpdater.push(accumulatedText);
515
- return;
516
- }
517
- case 'reasoning_delta': {
518
- // logDeduplicated('reasoning_delta');
519
- const fullReasoningText = event.fullText ?? '';
520
- const newReasoningText = fullReasoningText.slice(flushedReasoningLength);
521
- accumulatedReasoningText = newReasoningText;
522
- if (!newReasoningText.trim())
523
- return;
524
- reasoningUpdater.push(newReasoningText);
525
- return;
526
- }
527
- case 'tool_started': {
528
- // Flush reasoning state
529
- if (accumulatedReasoningText.trim()) {
530
- reasoningUpdater.flush();
531
- flushedReasoningLength += accumulatedReasoningText.length;
532
- accumulatedReasoningText = '';
533
- currentReasoningMessageId = null;
534
- }
535
- // Flush any accumulated text before showing the tool call
536
- if (accumulatedText.trim()) {
537
- const textMessage = {
538
- id: Date.now() + 1,
539
- sender: 'bot',
540
- text: accumulatedText,
541
- };
542
- appendMessages([textMessage]);
543
- accumulatedText = '';
544
- textWasFlushed = true;
545
- liveResponseUpdater.cancel();
546
- setLiveResponse(null);
547
- }
548
- const { toolCallId, toolName, arguments: rawArgs } = event;
549
- const args = (() => {
550
- if (typeof rawArgs !== 'string') {
551
- return rawArgs;
552
- }
553
- const trimmed = rawArgs.trim();
554
- if (!trimmed) {
555
- return rawArgs;
556
- }
557
- try {
558
- return JSON.parse(trimmed);
559
- }
560
- catch {
561
- return rawArgs;
562
- }
563
- })();
564
- const command = (() => {
565
- if (toolName === 'shell') {
566
- const cmd = args?.command ?? args?.commands;
567
- if (typeof cmd === 'string' && cmd.trim()) {
568
- return cmd;
569
- }
570
- if (Array.isArray(cmd) && cmd.length > 0) {
571
- return cmd.join('\n');
572
- }
573
- }
574
- if (toolName === 'grep' && args?.pattern) {
575
- return `grep "${args.pattern}" ${args.path ?? '.'}`;
576
- }
577
- if (toolName === 'search_replace') {
578
- return `search_replace "${args.search_content ?? ''}" → "${args.replace_content ?? ''}" ${args.path ?? ''}`;
579
- }
580
- if (toolName === 'apply_patch') {
581
- return `apply_patch ${args?.type ?? 'unknown'} ${args?.path ?? ''}`;
582
- }
583
- if (toolName === 'ask_mentor') {
584
- return `ask_mentor: ${args?.question ?? ''}`;
585
- }
586
- return `${toolName ?? 'unknown_tool'}`;
587
- })();
588
- const pendingMessage = {
589
- id: toolCallId ?? String(Date.now()),
590
- sender: 'command',
591
- status: 'running',
592
- command,
593
- output: '',
594
- callId: toolCallId,
595
- toolName,
596
- toolArgs: args,
597
- };
598
- appendMessages([pendingMessage]);
599
- return;
600
- }
601
- case 'command_message': {
602
- // logDeduplicated('command_message');
603
- const cmdMsg = event.message;
604
- const annotated = annotateCommandMessage(cmdMsg);
605
- const messagesToAdd = [];
606
- if (accumulatedReasoningText.trim()) {
607
- reasoningUpdater.flush();
608
- flushedReasoningLength +=
609
- accumulatedReasoningText.length;
610
- accumulatedReasoningText = '';
611
- currentReasoningMessageId = null;
612
- }
613
- if (accumulatedText.trim()) {
614
- const textMessage = {
615
- id: Date.now() + 1,
616
- sender: 'bot',
617
- text: accumulatedText,
618
- };
619
- messagesToAdd.push(textMessage);
620
- accumulatedText = '';
621
- textWasFlushed = true;
622
- }
623
- if (messagesToAdd.length > 0) {
624
- appendMessages(messagesToAdd);
625
- liveResponseUpdater.cancel();
626
- setLiveResponse(null);
627
- }
628
- // Replace pending message with completed one, or add new if not found
629
- setMessages(prev => {
630
- const pendingIndex = annotated.callId
631
- ? prev.findIndex(msg => msg.sender === 'command' &&
632
- msg.callId === annotated.callId &&
633
- msg.status === 'running')
634
- : -1;
635
- if (pendingIndex !== -1) {
636
- const next = [...prev];
637
- next[pendingIndex] = annotated;
638
- return trimMessages(next);
639
- }
640
- return trimMessages([...prev, annotated]);
641
- });
642
- return;
643
- }
644
- case 'retry': {
645
- const systemMessage = {
646
- id: Date.now(),
647
- sender: 'system',
648
- text: `Tool hallucination detected (${event.toolName}). Retrying... (Attempt ${event.attempt}/${event.maxRetries})`,
649
- };
650
- setMessages(prev => [...prev, systemMessage]);
651
- return;
652
- }
653
- default:
654
- return;
655
- }
656
- };
338
+ // Create event handler using extracted factory
339
+ const applyConversationEvent = createConversationEventHandler({
340
+ liveResponseUpdater,
341
+ reasoningUpdater,
342
+ appendMessages,
343
+ setMessages,
344
+ setLiveResponse,
345
+ trimMessages,
346
+ annotateCommandMessage,
347
+ }, streamingState);
657
348
  try {
658
349
  // Send a continuation message to resume work
659
350
  const continuationMessage = 'Please continue with your previous task.';
660
351
  const result = await conversationService.sendMessage(continuationMessage, {
661
352
  onEvent: applyConversationEvent,
662
353
  });
663
- applyServiceResult(result, accumulatedText, accumulatedReasoningText, textWasFlushed);
354
+ applyServiceResult(result, streamingState.accumulatedText, streamingState.accumulatedReasoningText, streamingState.textWasFlushed);
664
355
  }
665
356
  catch (error) {
666
357
  loggingService.error('Error in continuation after max turns', {
@@ -704,16 +395,12 @@ export const useConversation = ({ conversationService, loggingService, }) => {
704
395
  text: '',
705
396
  });
706
397
  const liveResponseUpdater = createLiveResponseUpdater(liveMessageId);
707
- // Track accumulated text so we can flush it before command messages
708
- let accumulatedText = '';
709
- let accumulatedReasoningText = '';
710
- let flushedReasoningLength = 0; // Track how much reasoning has been flushed
711
- let textWasFlushed = false;
712
- let currentReasoningMessageId = null; // Track current reasoning message ID
398
+ // Create streaming state object for this approval decision
399
+ const streamingState = createStreamingState();
713
400
  const reasoningUpdater = createStreamingUpdateCoordinator((newReasoningText) => {
714
401
  setMessages(prev => {
715
- if (currentReasoningMessageId !== null) {
716
- const index = prev.findIndex(msg => msg.id === currentReasoningMessageId);
402
+ if (streamingState.currentReasoningMessageId !== null) {
403
+ const index = prev.findIndex(msg => msg.id === streamingState.currentReasoningMessageId);
717
404
  if (index === -1)
718
405
  return prev;
719
406
  const current = prev[index];
@@ -725,7 +412,7 @@ export const useConversation = ({ conversationService, loggingService, }) => {
725
412
  return trimMessages(next);
726
413
  }
727
414
  const newId = Date.now();
728
- currentReasoningMessageId = newId;
415
+ streamingState.currentReasoningMessageId = newId;
729
416
  return trimMessages([
730
417
  ...prev,
731
418
  {
@@ -736,163 +423,21 @@ export const useConversation = ({ conversationService, loggingService, }) => {
736
423
  ]);
737
424
  });
738
425
  }, REASONING_RESPONSE_THROTTLE_MS);
739
- // Create event logger with deduplication for this approval decision
740
- // const {logDeduplicated, flush: flushLog} = createEventLogger();
741
- const applyConversationEvent = (event) => {
742
- switch (event.type) {
743
- case 'text_delta': {
744
- // logDeduplicated('text_delta');
745
- accumulatedText += event.delta;
746
- liveResponseUpdater.push(accumulatedText);
747
- return;
748
- }
749
- case 'reasoning_delta': {
750
- // logDeduplicated('reasoning_delta');
751
- const fullReasoningText = event.fullText ?? '';
752
- const newReasoningText = fullReasoningText.slice(flushedReasoningLength);
753
- accumulatedReasoningText = newReasoningText;
754
- if (!newReasoningText.trim())
755
- return;
756
- reasoningUpdater.push(newReasoningText);
757
- return;
758
- }
759
- case 'tool_started': {
760
- // Flush reasoning state
761
- if (accumulatedReasoningText.trim()) {
762
- reasoningUpdater.flush();
763
- flushedReasoningLength += accumulatedReasoningText.length;
764
- accumulatedReasoningText = '';
765
- currentReasoningMessageId = null;
766
- }
767
- // Flush any accumulated text before showing the tool call
768
- if (accumulatedText.trim()) {
769
- const textMessage = {
770
- id: Date.now() + 1,
771
- sender: 'bot',
772
- text: accumulatedText,
773
- };
774
- appendMessages([textMessage]);
775
- accumulatedText = '';
776
- textWasFlushed = true;
777
- liveResponseUpdater.cancel();
778
- setLiveResponse(null);
779
- }
780
- const { toolCallId, toolName, arguments: rawArgs } = event;
781
- // tool_started.arguments may be either an object or a JSON string
782
- // depending on provider. Normalize so we can render params.
783
- const args = (() => {
784
- if (typeof rawArgs !== 'string') {
785
- return rawArgs;
786
- }
787
- const trimmed = rawArgs.trim();
788
- if (!trimmed) {
789
- return rawArgs;
790
- }
791
- try {
792
- return JSON.parse(trimmed);
793
- }
794
- catch {
795
- return rawArgs;
796
- }
797
- })();
798
- const command = (() => {
799
- if (toolName === 'shell') {
800
- const cmd = args?.command ?? args?.commands;
801
- if (typeof cmd === 'string' && cmd.trim()) {
802
- return cmd;
803
- }
804
- if (Array.isArray(cmd) && cmd.length > 0) {
805
- return cmd.join('\n');
806
- }
807
- }
808
- if (toolName === 'grep' && args?.pattern) {
809
- return `grep "${args.pattern}" ${args.path ?? '.'}`;
810
- }
811
- if (toolName === 'search_replace') {
812
- return `search_replace "${args.search_content ?? ''}" → "${args.replace_content ?? ''}" ${args.path ?? ''}`;
813
- }
814
- if (toolName === 'apply_patch') {
815
- return `apply_patch ${args?.type ?? 'unknown'} ${args?.path ?? ''}`;
816
- }
817
- if (toolName === 'ask_mentor') {
818
- return `ask_mentor: ${args?.question ?? ''}`;
819
- }
820
- return `${toolName ?? 'unknown_tool'}`;
821
- })();
822
- const pendingMessage = {
823
- id: toolCallId ?? String(Date.now()),
824
- sender: 'command',
825
- status: 'running',
826
- command,
827
- output: '',
828
- callId: toolCallId,
829
- toolName,
830
- toolArgs: args,
831
- };
832
- appendMessages([pendingMessage]);
833
- return;
834
- }
835
- case 'command_message': {
836
- // logDeduplicated('command_message');
837
- const cmdMsg = event.message;
838
- const annotated = annotateCommandMessage(cmdMsg);
839
- const messagesToAdd = [];
840
- if (accumulatedReasoningText.trim()) {
841
- reasoningUpdater.flush();
842
- flushedReasoningLength +=
843
- accumulatedReasoningText.length;
844
- accumulatedReasoningText = '';
845
- currentReasoningMessageId = null;
846
- }
847
- if (accumulatedText.trim()) {
848
- const textMessage = {
849
- id: Date.now() + 1,
850
- sender: 'bot',
851
- text: accumulatedText,
852
- };
853
- messagesToAdd.push(textMessage);
854
- accumulatedText = '';
855
- textWasFlushed = true;
856
- }
857
- if (messagesToAdd.length > 0) {
858
- appendMessages(messagesToAdd);
859
- liveResponseUpdater.cancel();
860
- setLiveResponse(null);
861
- }
862
- // Replace pending message with completed one, or add new if not found
863
- setMessages(prev => {
864
- const pendingIndex = annotated.callId
865
- ? prev.findIndex(msg => msg.sender === 'command' &&
866
- msg.callId === annotated.callId &&
867
- msg.status === 'running')
868
- : -1;
869
- if (pendingIndex !== -1) {
870
- const next = [...prev];
871
- next[pendingIndex] = annotated;
872
- return trimMessages(next);
873
- }
874
- return trimMessages([...prev, annotated]);
875
- });
876
- return;
877
- }
878
- case 'retry': {
879
- const systemMessage = {
880
- id: Date.now(),
881
- sender: 'system',
882
- text: `Tool hallucination detected (${event.toolName}). Retrying... (Attempt ${event.attempt}/${event.maxRetries})`,
883
- };
884
- setMessages(prev => [...prev, systemMessage]);
885
- return;
886
- }
887
- default:
888
- return;
889
- }
890
- };
426
+ // Create event handler using extracted factory
427
+ const applyConversationEvent = createConversationEventHandler({
428
+ liveResponseUpdater,
429
+ reasoningUpdater,
430
+ appendMessages,
431
+ setMessages,
432
+ setLiveResponse,
433
+ trimMessages,
434
+ annotateCommandMessage,
435
+ }, streamingState);
891
436
  try {
892
437
  const result = await conversationService.handleApprovalDecision(answer, rejectionReason, {
893
438
  onEvent: applyConversationEvent,
894
439
  });
895
- applyServiceResult(result, accumulatedText, accumulatedReasoningText, textWasFlushed);
440
+ applyServiceResult(result, streamingState.accumulatedText, streamingState.accumulatedReasoningText, streamingState.textWasFlushed);
896
441
  }
897
442
  catch (error) {
898
443
  loggingService.error('Error in handleApprovalDecision', {