@ebowwa/coder 0.7.64 → 0.7.65

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 (101) hide show
  1. package/dist/index.js +36168 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +283 -173
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
@@ -1,537 +0,0 @@
1
- /**
2
- * Main Interactive TUI Component
3
- * Orchestrates all sub-components and manages state and agent loop
4
- *
5
- * Uses:
6
- * - MessageStore: Centralized message state management
7
- * - InputContext: Centralized keyboard input handling
8
- */
9
-
10
- import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
11
- import { Box, Text, useApp, useStdout } from "ink";
12
- import type { ExtendedThinkingConfig } from "../../../../types/index.js";
13
- import { agentLoop } from "../../../../core/agent-loop.js";
14
- import { getGitStatus } from "../../../../core/git-status.js";
15
- import { createStreamHighlighter } from "../../../../core/stream-highlighter.js";
16
- import { calculateContextInfo } from "../shared/status-line.js";
17
- import { spinnerFrames } from "./spinner.js";
18
- import { MessageArea } from "./MessageArea.js";
19
- import { InputField, setGlobalInput } from "./InputField.js";
20
- import { handleCommand } from "./commands.js";
21
- import { useNativeInput, KeyEvents } from "./useNativeInput.js";
22
- import { InputProvider } from "./InputContext.js";
23
- import { MessageStoreProvider, useMessageStore } from "./MessageStore.js";
24
- import type { InteractiveTUIProps, MessageSubType } from "./types.js";
25
-
26
- /**
27
- * Estimate token count from text
28
- * Uses ~4 characters per token as rough approximation
29
- */
30
- function estimateTokens(text: string): number {
31
- if (!text) return 0;
32
- return Math.ceil(text.length / 4);
33
- }
34
-
35
- /**
36
- * Estimate total tokens from messages
37
- */
38
- function estimateMessagesTokens(messages: import("../../../../types/index.js").Message[]): number {
39
- let total = 0;
40
- for (const msg of messages) {
41
- if (typeof msg.content === "string") {
42
- total += estimateTokens(msg.content);
43
- } else if (Array.isArray(msg.content)) {
44
- for (const block of msg.content) {
45
- if (block.type === "text") {
46
- total += estimateTokens(block.text);
47
- } else if (block.type === "tool_use") {
48
- total += estimateTokens(JSON.stringify(block.input));
49
- } else if (block.type === "tool_result") {
50
- if (typeof block.content === "string") {
51
- total += estimateTokens(block.content);
52
- }
53
- }
54
- }
55
- }
56
- }
57
- return total;
58
- }
59
-
60
- /**
61
- * Inner component that uses MessageStore
62
- */
63
- function InteractiveTUIInner({
64
- apiKey,
65
- model: initialModel,
66
- permissionMode,
67
- maxTokens,
68
- systemPrompt: initialSystemPrompt,
69
- tools,
70
- hookManager,
71
- sessionStore,
72
- sessionId,
73
- setSessionId,
74
- workingDirectory,
75
- onExit,
76
- }: InteractiveTUIProps) {
77
- // Message store
78
- const {
79
- messages,
80
- apiMessages,
81
- addMessage,
82
- addApiMessages,
83
- addSystem,
84
- tokenCount,
85
- setTokenCount,
86
- } = useMessageStore();
87
-
88
- // UI state - use refs for immediate input display updates
89
- const inputRef = useRef("");
90
- const cursorRef = useRef(0);
91
- const [isLoading, setIsLoading] = useState(false);
92
-
93
- // Update input and sync to global state for InputField
94
- // NOTE: Use inputRef.current and cursorRef.current directly in callbacks
95
- // to avoid stale closure values. Don't use inputValue/cursorPos variables.
96
- const setInputValue = useCallback((value: string) => {
97
- inputRef.current = value;
98
- setGlobalInput(value, cursorRef.current);
99
- }, []);
100
-
101
- const setCursorPos = useCallback((pos: number | ((prev: number) => number)) => {
102
- const newPos = typeof pos === "function" ? pos(cursorRef.current) : pos;
103
- cursorRef.current = newPos;
104
- setGlobalInput(inputRef.current, newPos);
105
- }, []);
106
-
107
- const [totalCost, setTotalCost] = useState(0);
108
- const [spinnerFrame, setSpinnerFrame] = useState("⠋");
109
- const [model, setModel] = useState(initialModel);
110
- const [systemPrompt] = useState(initialSystemPrompt);
111
- const [streamingText, setStreamingText] = useState("");
112
- const [scrollOffset, setScrollOffset] = useState(0);
113
- const [sessionSelectMode, setSessionSelectMode] = useState(false);
114
- const [selectableSessions, setSelectableSessions] = useState<Array<{ id: string; messageCount: number; metadata?: Record<string, unknown> }>>([]);
115
- const [helpMode, setHelpMode] = useState(false);
116
- const [helpSection, setHelpSection] = useState(0);
117
-
118
- // Input history
119
- const [inputHistory, setInputHistory] = useState<string[]>([]);
120
- const [historyIndex, setHistoryIndex] = useState(-1);
121
- const [savedInput, setSavedInput] = useState("");
122
-
123
- const { exit } = useApp();
124
- const { stdout } = useStdout();
125
- const frameRef = useRef(0);
126
- const isProcessingRef = useRef(false);
127
- const highlighterRef = useRef(createStreamHighlighter());
128
-
129
- // Calculate terminal layout
130
- const terminalHeight = stdout.rows || 24;
131
- const inputHeight = 3;
132
- const statusHeight = 3;
133
- const messageHeight = terminalHeight - inputHeight - statusHeight;
134
-
135
- // Calculate context warning
136
- const contextInfo = calculateContextInfo(tokenCount, model);
137
- const contextWarning = contextInfo.isCritical
138
- ? "Context critical! Use /compact or start new conversation"
139
- : contextInfo.isLow
140
- ? `Context low: ${contextInfo.percentRemaining.toFixed(0)}% remaining`
141
- : null;
142
-
143
- // Auto-scroll to bottom when new messages arrive
144
- useEffect(() => {
145
- setScrollOffset(0);
146
- }, [messages.length]);
147
-
148
- // Spinner animation
149
- useEffect(() => {
150
- if (!isLoading) return;
151
-
152
- const interval = setInterval(() => {
153
- frameRef.current = (frameRef.current + 1) % spinnerFrames.length;
154
- const frame = spinnerFrames[frameRef.current];
155
- if (frame) setSpinnerFrame(frame);
156
- }, 80);
157
-
158
- return () => clearInterval(interval);
159
- }, [isLoading]);
160
-
161
- // Process a message
162
- const processMessage = useCallback(async (input: string, messageAlreadyAdded = false) => {
163
- if (isProcessingRef.current) return;
164
- isProcessingRef.current = true;
165
-
166
- // Add user message to UI if not already added
167
- if (!messageAlreadyAdded) {
168
- addMessage({ role: "user", content: input });
169
- }
170
-
171
- setIsLoading(true);
172
- setStreamingText("");
173
- highlighterRef.current = createStreamHighlighter();
174
-
175
- try {
176
- // Execute UserPromptSubmit hook
177
- const hookResult = await hookManager.execute("UserPromptSubmit", {
178
- prompt: input,
179
- session_id: sessionId,
180
- });
181
-
182
- if (hookResult.decision === "deny" || hookResult.decision === "block") {
183
- addSystem(`Input blocked: ${hookResult.reason || "Security policy"}`);
184
- return;
185
- }
186
-
187
- const processedInput = (hookResult.modified_input?.prompt as string) ?? input;
188
-
189
- // Build messages for API
190
- const newUserMsg = {
191
- role: "user" as const,
192
- content: [{ type: "text" as const, text: processedInput }],
193
- };
194
- const messagesForApi = [...apiMessages, newUserMsg];
195
-
196
- // Get git status
197
- const gitStatus = await getGitStatus(workingDirectory);
198
-
199
- // Run agent loop
200
- const result = await agentLoop(messagesForApi, {
201
- apiKey,
202
- model,
203
- maxTokens,
204
- systemPrompt,
205
- tools,
206
- permissionMode,
207
- workingDirectory,
208
- gitStatus,
209
- extendedThinking: undefined,
210
- hookManager,
211
- sessionId,
212
- onText: (text) => {
213
- setStreamingText((prev) => prev + text);
214
- },
215
- onThinking: () => {
216
- // Could show thinking in UI
217
- },
218
- onToolUse: (toolUse) => {
219
- addSystem(`[Using: ${toolUse.name}]`, "tool_call", toolUse.name);
220
- },
221
- onToolResult: (toolResult) => {
222
- if (toolResult.result.is_error) {
223
- addSystem(`[Tool ${toolResult.id}: Error]`, "tool_result", undefined, true);
224
- }
225
- },
226
- onMetrics: async (metrics) => {
227
- const apiTokens = metrics.usage.input_tokens + metrics.usage.output_tokens;
228
- if (apiTokens > 0) {
229
- setTokenCount(apiTokens);
230
- }
231
- await sessionStore.saveMetrics(metrics);
232
- },
233
- });
234
-
235
- // Update API messages (MessageStore will convert to UI messages)
236
- // Note: result.messages already includes newUserMsg, so we don't add it again
237
- // Only add the NEW messages from the result (skip ones we already have)
238
- addApiMessages(result.messages.slice(apiMessages.length));
239
- setTotalCost((prev) => prev + result.totalCost);
240
-
241
- // Estimate tokens from final messages
242
- const estimatedTokens = estimateMessagesTokens(result.messages);
243
- setTokenCount(estimatedTokens);
244
-
245
- // Save to session
246
- const lastUserMsg = result.messages[result.messages.length - 2];
247
- const lastAssistantMsg = result.messages[result.messages.length - 1];
248
- if (lastUserMsg) await sessionStore.saveMessage(lastUserMsg);
249
- if (lastAssistantMsg) await sessionStore.saveMessage(lastAssistantMsg);
250
-
251
- } catch (error) {
252
- const errorMessage = error instanceof Error ? error.message : String(error);
253
- addSystem(`Error: ${errorMessage}`, "error");
254
- } finally {
255
- setIsLoading(false);
256
- isProcessingRef.current = false;
257
- setStreamingText("");
258
- }
259
- }, [apiMessages, apiKey, model, maxTokens, systemPrompt, tools, permissionMode, workingDirectory, hookManager, sessionId, sessionStore, addMessage, addSystem, addApiMessages, setTokenCount]);
260
-
261
- // Handle commands
262
- const handleCommandWrapper = useCallback(async (cmd: string) => {
263
- // Import command context dynamically to avoid circular deps
264
- const { setMessages: setExternalMessages, processedCountRef } = {
265
- setMessages: () => {}, // MessageStore handles this now
266
- processedCountRef: { current: apiMessages.length },
267
- };
268
-
269
- await handleCommand(cmd, {
270
- sessionId,
271
- setSessionId,
272
- model,
273
- setModel,
274
- apiMessages,
275
- setApiMessages: (msgs) => addApiMessages(msgs.slice(apiMessages.length)),
276
- setMessages: setExternalMessages,
277
- processedCountRef,
278
- totalCost,
279
- setTotalCost,
280
- totalTokens: tokenCount,
281
- setTotalTokens: setTokenCount,
282
- permissionMode,
283
- tools,
284
- workingDirectory,
285
- sessionStore,
286
- addSystemMessage: (content: string, subType?: MessageSubType, toolName?: string, isError?: boolean) => {
287
- addSystem(content, subType, toolName, isError);
288
- },
289
- messagesLength: messages.length,
290
- onExit,
291
- exit,
292
- sessionSelectMode,
293
- setSessionSelectMode,
294
- setSelectableSessions,
295
- helpMode,
296
- setHelpMode,
297
- helpSection,
298
- setHelpSection,
299
- });
300
- }, [sessionId, setSessionId, model, apiMessages, addApiMessages, totalCost, tokenCount, setTokenCount, permissionMode, tools, workingDirectory, sessionStore, addSystem, messages.length, onExit, exit, sessionSelectMode, helpMode, helpSection]);
301
-
302
- // Handle input with native terminal input
303
- useNativeInput({
304
- isActive: true,
305
- onKey: (event) => {
306
- // Scroll handling
307
- if (KeyEvents.isPageUp(event)) {
308
- setScrollOffset((prev) => prev + 5);
309
- return;
310
- }
311
-
312
- if (KeyEvents.isPageDown(event)) {
313
- setScrollOffset((prev) => Math.max(0, prev - 5));
314
- return;
315
- }
316
-
317
- if (KeyEvents.isShiftUp(event)) {
318
- setScrollOffset((prev) => prev + 1);
319
- return;
320
- }
321
-
322
- if (KeyEvents.isShiftDown(event)) {
323
- setScrollOffset((prev) => Math.max(0, prev - 1));
324
- return;
325
- }
326
-
327
- // Ctrl+C to exit
328
- if (KeyEvents.isCtrlC(event)) {
329
- onExit();
330
- exit();
331
- return;
332
- }
333
-
334
- if (isLoading) return;
335
-
336
- // Help mode navigation
337
- if (helpMode) {
338
- const HELP_SECTIONS_COUNT = 5;
339
-
340
- if (event.code === "escape" || event.code === "q") {
341
- setHelpMode(false);
342
- return;
343
- }
344
-
345
- if (event.code === "tab" || KeyEvents.isRight(event)) {
346
- setHelpSection((prev) => (prev + 1) % HELP_SECTIONS_COUNT);
347
- return;
348
- }
349
-
350
- if (KeyEvents.isLeft(event)) {
351
- setHelpSection((prev) => (prev - 1 + HELP_SECTIONS_COUNT) % HELP_SECTIONS_COUNT);
352
- return;
353
- }
354
-
355
- return;
356
- }
357
-
358
- // Session selection mode
359
- if (sessionSelectMode) {
360
- const num = parseInt(event.code, 10);
361
- if (!isNaN(num) && num >= 1 && num <= selectableSessions.length) {
362
- const selectedSession = selectableSessions[num - 1];
363
- if (selectedSession) {
364
- setSessionSelectMode(false);
365
- setSelectableSessions([]);
366
- handleCommandWrapper(`/resume ${selectedSession.id}`);
367
- }
368
- } else if (KeyEvents.isEnter(event) || (event.code && isNaN(num))) {
369
- setSessionSelectMode(false);
370
- setSelectableSessions([]);
371
- addSystem("Session selection cancelled.");
372
- }
373
- return;
374
- }
375
-
376
- // Submit on Enter
377
- if (KeyEvents.isEnter(event)) {
378
- // Prevent duplicate submissions while processing
379
- if (isProcessingRef.current) return;
380
-
381
- const currentInput = inputRef.current;
382
- if (currentInput.trim()) {
383
- // Capture value BEFORE clearing
384
- const valueToSubmit = currentInput;
385
-
386
- // Clear input IMMEDIATELY to prevent duplicate submissions
387
- // This must happen before any async operations
388
- inputRef.current = "";
389
- setGlobalInput("", 0);
390
-
391
- // Add user message to UI
392
- addMessage({ role: "user", content: valueToSubmit });
393
-
394
- // Clear cursor and history state
395
- setCursorPos(0);
396
- setHistoryIndex(-1);
397
- setSavedInput("");
398
-
399
- // Update history
400
- if (!valueToSubmit.startsWith("/") && valueToSubmit !== inputHistory[0]) {
401
- setInputHistory((prev) => [valueToSubmit, ...prev].slice(0, 100));
402
- }
403
-
404
- // Process after UI updates
405
- setTimeout(() => {
406
- if (valueToSubmit.startsWith("/")) {
407
- handleCommandWrapper(valueToSubmit);
408
- } else {
409
- processMessage(valueToSubmit, true); // true = message already added
410
- }
411
- }, 50);
412
- }
413
- return;
414
- }
415
-
416
- // History navigation
417
- if (KeyEvents.isUp(event)) {
418
- if (inputHistory.length > 0) {
419
- if (historyIndex === -1) {
420
- setSavedInput(inputRef.current);
421
- }
422
- const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1);
423
- setHistoryIndex(newIndex);
424
- setInputValue(inputHistory[newIndex] ?? "");
425
- setCursorPos((inputHistory[newIndex] ?? "").length);
426
- }
427
- return;
428
- }
429
-
430
- if (KeyEvents.isDown(event)) {
431
- if (historyIndex > 0) {
432
- const newIndex = historyIndex - 1;
433
- setHistoryIndex(newIndex);
434
- setInputValue(inputHistory[newIndex] ?? "");
435
- setCursorPos((inputHistory[newIndex] ?? "").length);
436
- } else if (historyIndex === 0) {
437
- setHistoryIndex(-1);
438
- setInputValue(savedInput);
439
- setCursorPos(savedInput.length);
440
- }
441
- return;
442
- }
443
-
444
- // Text editing
445
- if (KeyEvents.isBackspace(event)) {
446
- const currentInput = inputRef.current;
447
- const currentCursor = cursorRef.current;
448
- if (currentCursor > 0) {
449
- const newVal = currentInput.slice(0, currentCursor - 1) + currentInput.slice(currentCursor);
450
- setInputValue(newVal);
451
- setCursorPos((p) => p - 1);
452
- }
453
- return;
454
- }
455
-
456
- if (KeyEvents.isDelete(event)) {
457
- const currentInput = inputRef.current;
458
- const currentCursor = cursorRef.current;
459
- if (currentCursor < currentInput.length) {
460
- const newVal = currentInput.slice(0, currentCursor) + currentInput.slice(currentCursor + 1);
461
- setInputValue(newVal);
462
- }
463
- return;
464
- }
465
-
466
- if (KeyEvents.isLeft(event)) {
467
- setCursorPos((p) => Math.max(0, p - 1));
468
- return;
469
- }
470
-
471
- if (KeyEvents.isRight(event)) {
472
- setCursorPos((p) => Math.min(inputRef.current.length, p + 1));
473
- return;
474
- }
475
-
476
- if (KeyEvents.isHome(event) || KeyEvents.isCtrlA(event)) {
477
- setCursorPos(0);
478
- return;
479
- }
480
-
481
- if (KeyEvents.isEnd(event) || KeyEvents.isCtrlE(event)) {
482
- setCursorPos(inputRef.current.length);
483
- return;
484
- }
485
-
486
- // Regular character
487
- if (KeyEvents.isPrintable(event)) {
488
- if (historyIndex !== -1) {
489
- setHistoryIndex(-1);
490
- setSavedInput("");
491
- }
492
- const currentInput = inputRef.current;
493
- const currentCursor = cursorRef.current;
494
- setInputValue(currentInput.slice(0, currentCursor) + event.code + currentInput.slice(currentCursor));
495
- setCursorPos((p) => p + 1);
496
- }
497
- },
498
- });
499
-
500
- return (
501
- <InputProvider initialBlocked={isLoading}>
502
- <Box flexDirection="column" width="100%">
503
- <MessageArea
504
- messages={messages}
505
- isLoading={isLoading}
506
- spinnerFrame={spinnerFrame}
507
- height={messageHeight}
508
- scrollOffset={scrollOffset}
509
- contextWarning={contextWarning}
510
- streamingText={streamingText}
511
- />
512
-
513
- <Text dimColor>
514
- {isLoading ? spinnerFrame : ""} Context: {tokenCount} tokens | {permissionMode}
515
- </Text>
516
-
517
- <InputField
518
- placeholder="Type your message... (/help for commands)"
519
- isActive={!isLoading}
520
- />
521
- </Box>
522
- </InputProvider>
523
- );
524
- }
525
-
526
- /**
527
- * Main Interactive TUI Component with providers
528
- */
529
- function InteractiveTUI(props: InteractiveTUIProps) {
530
- return (
531
- <MessageStoreProvider initialMessages={props.initialMessages}>
532
- <InteractiveTUIInner {...props} />
533
- </MessageStoreProvider>
534
- );
535
- }
536
-
537
- export default InteractiveTUI;
@@ -1,107 +0,0 @@
1
- /** @jsx React.createElement */
2
- /** @jsxFrag React.Fragment */
3
- /**
4
- * Message Area Component - Simple plain text display
5
- */
6
-
7
- import React from "react";
8
- import { Text } from "ink";
9
- import type { MessageAreaProps, UIMessage, MessageSubType } from "./types.js";
10
- import { VERSION } from "../shared/status-line.js";
11
-
12
- function getMessageColor(role: UIMessage["role"], subType?: MessageSubType, isError?: boolean): string {
13
- if (role === "system") {
14
- if (isError || subType === "error") return "red";
15
- if (subType === "tool_call") return "blue";
16
- if (subType === "tool_result") return "green";
17
- if (subType === "hook") return "yellow";
18
- if (subType === "thinking") return "gray";
19
- if (subType === "info") return "cyan";
20
- return "yellow";
21
- }
22
- switch (role) {
23
- case "user": return "cyan";
24
- case "assistant": return "magenta";
25
- default: return "white";
26
- }
27
- }
28
-
29
- function getMessageLabel(message: UIMessage): string {
30
- const { role, subType, toolName, isError } = message;
31
-
32
- if (role === "system" && subType) {
33
- switch (subType) {
34
- case "tool_call": return toolName ? `[${toolName}]` : "[Tool]";
35
- case "tool_result": return isError ? "[Error]" : "[Result]";
36
- case "hook": return "[Hook]";
37
- case "error": return "[Error]";
38
- case "thinking": return "[Thinking]";
39
- case "info": return "[Info]";
40
- }
41
- }
42
-
43
- switch (role) {
44
- case "user": return "You:";
45
- case "system": return "[System]";
46
- case "assistant": return "Claude:";
47
- default: return "";
48
- }
49
- }
50
-
51
- export function MessageArea({
52
- messages,
53
- isLoading,
54
- spinnerFrame,
55
- height,
56
- scrollOffset = 0,
57
- contextWarning,
58
- streamingText,
59
- }: MessageAreaProps) {
60
- const totalMessages = messages.length;
61
- const maxVisibleMessages = 50;
62
- const endIdx = totalMessages - scrollOffset;
63
- const startIdx = Math.max(0, endIdx - maxVisibleMessages);
64
- const visibleMessages = messages.slice(startIdx, endIdx);
65
-
66
- const isEmpty = visibleMessages.length === 0 && !isLoading && !streamingText;
67
-
68
- return (
69
- <>
70
- {contextWarning && (
71
- <Text color="yellow" bold>Warning: {contextWarning}{"\n"}</Text>
72
- )}
73
-
74
- {isEmpty && (
75
- <Text dimColor>Welcome to Coder v{VERSION}. Type your message or /help for commands.{"\n"}</Text>
76
- )}
77
-
78
- {visibleMessages.map((msg) => {
79
- const displayContent = msg.content.length > 2000
80
- ? msg.content.slice(0, 2000) + "..."
81
- : msg.content;
82
- const color = getMessageColor(msg.role, msg.subType, msg.isError);
83
- const label = getMessageLabel(msg);
84
-
85
- return (
86
- <Text key={msg.id}>
87
- <Text bold color={color}>{label} </Text>
88
- <Text dimColor={msg.role === "system"}>{displayContent}{"\n"}</Text>
89
- </Text>
90
- );
91
- })}
92
-
93
- {streamingText && (
94
- <Text>
95
- <Text bold color="magenta">Claude: </Text>
96
- <Text dimColor>{streamingText.length > 500 ? "..." + streamingText.slice(-500) : streamingText}{"\n"}</Text>
97
- </Text>
98
- )}
99
-
100
- {isLoading && !streamingText && (
101
- <Text color="cyan">{spinnerFrame} Processing...{"\n"}</Text>
102
- )}
103
- </>
104
- );
105
- }
106
-
107
- export default MessageArea;