wave-code 0.0.5 → 0.0.6

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 (57) hide show
  1. package/README.md +2 -2
  2. package/dist/components/ChatInterface.d.ts.map +1 -1
  3. package/dist/components/ChatInterface.js +4 -24
  4. package/dist/components/CommandSelector.js +4 -4
  5. package/dist/components/DiffViewer.d.ts +1 -1
  6. package/dist/components/DiffViewer.d.ts.map +1 -1
  7. package/dist/components/DiffViewer.js +15 -15
  8. package/dist/components/FileSelector.js +2 -2
  9. package/dist/components/InputBox.d.ts.map +1 -1
  10. package/dist/components/InputBox.js +21 -50
  11. package/dist/components/Markdown.d.ts +6 -0
  12. package/dist/components/Markdown.d.ts.map +1 -0
  13. package/dist/components/Markdown.js +22 -0
  14. package/dist/components/MessageItem.d.ts +9 -0
  15. package/dist/components/MessageItem.d.ts.map +1 -0
  16. package/dist/components/MessageItem.js +15 -0
  17. package/dist/components/MessageList.d.ts +1 -1
  18. package/dist/components/MessageList.d.ts.map +1 -1
  19. package/dist/components/MessageList.js +33 -33
  20. package/dist/components/SubagentBlock.d.ts +0 -1
  21. package/dist/components/SubagentBlock.d.ts.map +1 -1
  22. package/dist/components/SubagentBlock.js +29 -30
  23. package/dist/components/ToolResultDisplay.js +5 -5
  24. package/dist/contexts/useChat.d.ts +2 -2
  25. package/dist/contexts/useChat.d.ts.map +1 -1
  26. package/dist/contexts/useChat.js +18 -9
  27. package/dist/hooks/useInputManager.d.ts +3 -1
  28. package/dist/hooks/useInputManager.d.ts.map +1 -1
  29. package/dist/hooks/useInputManager.js +15 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +9 -2
  32. package/dist/managers/InputManager.d.ts +3 -1
  33. package/dist/managers/InputManager.d.ts.map +1 -1
  34. package/dist/managers/InputManager.js +44 -24
  35. package/dist/print-cli.d.ts +1 -0
  36. package/dist/print-cli.d.ts.map +1 -1
  37. package/dist/print-cli.js +88 -23
  38. package/dist/utils/usageSummary.d.ts +6 -0
  39. package/dist/utils/usageSummary.d.ts.map +1 -1
  40. package/dist/utils/usageSummary.js +72 -0
  41. package/package.json +10 -6
  42. package/src/components/ChatInterface.tsx +13 -43
  43. package/src/components/CommandSelector.tsx +5 -5
  44. package/src/components/DiffViewer.tsx +18 -16
  45. package/src/components/FileSelector.tsx +2 -2
  46. package/src/components/InputBox.tsx +22 -74
  47. package/src/components/Markdown.tsx +29 -0
  48. package/src/components/MessageItem.tsx +104 -0
  49. package/src/components/MessageList.tsx +142 -202
  50. package/src/components/SubagentBlock.tsx +56 -84
  51. package/src/components/ToolResultDisplay.tsx +5 -5
  52. package/src/contexts/useChat.tsx +22 -13
  53. package/src/hooks/useInputManager.ts +21 -3
  54. package/src/index.ts +12 -2
  55. package/src/managers/InputManager.ts +55 -25
  56. package/src/print-cli.ts +103 -21
  57. package/src/utils/usageSummary.ts +109 -0
@@ -23,6 +23,31 @@ export function calculateTokenSummary(usages) {
23
23
  summary.prompt_tokens += usage.prompt_tokens;
24
24
  summary.completion_tokens += usage.completion_tokens;
25
25
  summary.total_tokens += usage.total_tokens;
26
+ // Handle cache tokens if present and non-zero
27
+ if (usage.cache_read_input_tokens && usage.cache_read_input_tokens > 0) {
28
+ summary.cache_read_input_tokens =
29
+ (summary.cache_read_input_tokens || 0) + usage.cache_read_input_tokens;
30
+ }
31
+ if (usage.cache_creation_input_tokens &&
32
+ usage.cache_creation_input_tokens > 0) {
33
+ summary.cache_creation_input_tokens =
34
+ (summary.cache_creation_input_tokens || 0) +
35
+ usage.cache_creation_input_tokens;
36
+ }
37
+ if (usage.cache_creation &&
38
+ (usage.cache_creation.ephemeral_5m_input_tokens > 0 ||
39
+ usage.cache_creation.ephemeral_1h_input_tokens > 0)) {
40
+ if (!summary.cache_creation) {
41
+ summary.cache_creation = {
42
+ ephemeral_5m_input_tokens: 0,
43
+ ephemeral_1h_input_tokens: 0,
44
+ };
45
+ }
46
+ summary.cache_creation.ephemeral_5m_input_tokens +=
47
+ usage.cache_creation.ephemeral_5m_input_tokens || 0;
48
+ summary.cache_creation.ephemeral_1h_input_tokens +=
49
+ usage.cache_creation.ephemeral_1h_input_tokens || 0;
50
+ }
26
51
  // Track operation types
27
52
  if (usage.operation_type === "agent") {
28
53
  summary.operations.agent_calls += 1;
@@ -59,11 +84,43 @@ export function displayUsageSummary(usages, sessionFilePath) {
59
84
  let totalTokens = 0;
60
85
  let totalAgentCalls = 0;
61
86
  let totalCompressions = 0;
87
+ let totalCacheRead = 0;
88
+ let totalCacheCreation = 0;
89
+ let totalCache5m = 0;
90
+ let totalCache1h = 0;
91
+ let hasCacheData = false;
62
92
  for (const [, summary] of Object.entries(summaries)) {
63
93
  console.log(`Model: ${summary.model}`);
64
94
  console.log(` Prompt tokens: ${summary.prompt_tokens.toLocaleString()}`);
65
95
  console.log(` Completion tokens: ${summary.completion_tokens.toLocaleString()}`);
66
96
  console.log(` Total tokens: ${summary.total_tokens.toLocaleString()}`);
97
+ // Display cache information if available
98
+ if (summary.cache_read_input_tokens ||
99
+ summary.cache_creation_input_tokens ||
100
+ summary.cache_creation) {
101
+ hasCacheData = true;
102
+ console.log(" Cache Usage:");
103
+ if (summary.cache_read_input_tokens &&
104
+ summary.cache_read_input_tokens > 0) {
105
+ console.log(` Read from cache: ${summary.cache_read_input_tokens.toLocaleString()} tokens`);
106
+ totalCacheRead += summary.cache_read_input_tokens;
107
+ }
108
+ if (summary.cache_creation_input_tokens &&
109
+ summary.cache_creation_input_tokens > 0) {
110
+ console.log(` Created cache: ${summary.cache_creation_input_tokens.toLocaleString()} tokens`);
111
+ totalCacheCreation += summary.cache_creation_input_tokens;
112
+ }
113
+ if (summary.cache_creation) {
114
+ if (summary.cache_creation.ephemeral_5m_input_tokens > 0) {
115
+ console.log(` 5m cache: ${summary.cache_creation.ephemeral_5m_input_tokens.toLocaleString()} tokens`);
116
+ totalCache5m += summary.cache_creation.ephemeral_5m_input_tokens;
117
+ }
118
+ if (summary.cache_creation.ephemeral_1h_input_tokens > 0) {
119
+ console.log(` 1h cache: ${summary.cache_creation.ephemeral_1h_input_tokens.toLocaleString()} tokens`);
120
+ totalCache1h += summary.cache_creation.ephemeral_1h_input_tokens;
121
+ }
122
+ }
123
+ }
67
124
  console.log(` Operations: ${summary.operations.agent_calls} agent calls, ${summary.operations.compressions} compressions`);
68
125
  console.log();
69
126
  totalPrompt += summary.prompt_tokens;
@@ -77,6 +134,21 @@ export function displayUsageSummary(usages, sessionFilePath) {
77
134
  console.log(` Prompt tokens: ${totalPrompt.toLocaleString()}`);
78
135
  console.log(` Completion tokens: ${totalCompletion.toLocaleString()}`);
79
136
  console.log(` Total tokens: ${totalTokens.toLocaleString()}`);
137
+ if (hasCacheData) {
138
+ console.log(" Cache Usage:");
139
+ if (totalCacheRead > 0) {
140
+ console.log(` Read from cache: ${totalCacheRead.toLocaleString()} tokens`);
141
+ }
142
+ if (totalCacheCreation > 0) {
143
+ console.log(` Created cache: ${totalCacheCreation.toLocaleString()} tokens`);
144
+ }
145
+ if (totalCache5m > 0) {
146
+ console.log(` 5m cache: ${totalCache5m.toLocaleString()} tokens`);
147
+ }
148
+ if (totalCache1h > 0) {
149
+ console.log(` 1h cache: ${totalCache1h.toLocaleString()} tokens`);
150
+ }
151
+ }
80
152
  console.log(` Operations: ${totalAgentCalls} agent calls, ${totalCompressions} compressions`);
81
153
  }
82
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "keywords": [
6
6
  "ai",
@@ -24,14 +24,18 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
- "ink": "^6.0.1",
28
- "react": "^19.1.0",
29
- "yargs": "^17.7.2",
27
+ "chalk": "^5.6.2",
30
28
  "diff": "^8.0.2",
31
29
  "glob": "^11.0.3",
32
- "wave-agent-sdk": "0.0.7"
30
+ "ink": "^6.5.1",
31
+ "marked": "^11.2.0",
32
+ "marked-terminal": "^7.3.0",
33
+ "react": "^19.1.0",
34
+ "yargs": "^17.7.2",
35
+ "wave-agent-sdk": "0.0.8"
33
36
  },
34
37
  "devDependencies": {
38
+ "@types/marked-terminal": "^6.1.1",
35
39
  "@types/react": "^19.1.8",
36
40
  "@types/yargs": "^17.0.0",
37
41
  "eslint-plugin-react": "^7.37.5",
@@ -52,7 +56,7 @@
52
56
  "scripts": {
53
57
  "build": "rimraf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
54
58
  "type-check": "tsc --noEmit --incremental",
55
- "dev": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
59
+ "watch": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
56
60
  "test": "vitest run",
57
61
  "lint": "eslint --cache",
58
62
  "format": "prettier --write ."
@@ -1,9 +1,8 @@
1
- import React, { useRef, useEffect } from "react";
1
+ import React from "react";
2
2
  import { Box } from "ink";
3
3
  import { MessageList } from "./MessageList.js";
4
4
  import { InputBox } from "./InputBox.js";
5
5
  import { useChat } from "../contexts/useChat.js";
6
- import type { Message } from "wave-agent-sdk";
7
6
 
8
7
  export const ChatInterface: React.FC = () => {
9
8
  const {
@@ -19,54 +18,25 @@ export const ChatInterface: React.FC = () => {
19
18
  connectMcpServer,
20
19
  disconnectMcpServer,
21
20
  isExpanded,
21
+ sessionId,
22
22
  latestTotalTokens,
23
23
  slashCommands,
24
24
  hasSlashCommand,
25
25
  } = useChat();
26
26
 
27
- // Create a ref to store messages in expanded mode
28
- const expandedMessagesRef = useRef<Message[]>([]);
29
-
30
- useEffect(() => {
31
- // Only sync when collapsed
32
- if (!isExpanded) {
33
- expandedMessagesRef.current = messages.map((message, index) => {
34
- // If it's the last message, deep copy its blocks
35
- if (index === messages.length - 1) {
36
- return {
37
- ...message,
38
- blocks: message.blocks.map((block) => ({ ...block })),
39
- };
40
- }
41
- return message;
42
- });
43
- }
44
- }, [isExpanded, messages]);
27
+ if (!sessionId) return null;
45
28
 
46
29
  return (
47
- <Box flexDirection="column" height="100%">
48
- <Box flexGrow={1} flexDirection="column" paddingX={1}>
49
- {isExpanded ? (
50
- // Expanded mode uses messages from ref, loading and tokens are hardcoded to false and 0
51
- <MessageList
52
- messages={expandedMessagesRef.current}
53
- isLoading={false}
54
- isCommandRunning={false}
55
- latestTotalTokens={0}
56
- isExpanded={true}
57
- />
58
- ) : (
59
- // Normal mode uses real-time state
60
- <MessageList
61
- messages={messages}
62
- isLoading={isLoading}
63
- isCommandRunning={isCommandRunning}
64
- isCompressing={isCompressing}
65
- latestTotalTokens={latestTotalTokens}
66
- isExpanded={false}
67
- />
68
- )}
69
- </Box>
30
+ <Box flexDirection="column" height="100%" paddingY={1}>
31
+ <MessageList
32
+ messages={messages}
33
+ isLoading={isLoading}
34
+ isCommandRunning={isCommandRunning}
35
+ isCompressing={isCompressing}
36
+ latestTotalTokens={latestTotalTokens}
37
+ isExpanded={isExpanded}
38
+ key={String(isExpanded) + sessionId}
39
+ />
70
40
 
71
41
  {!isExpanded && (
72
42
  <InputBox
@@ -41,7 +41,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
41
41
  const filteredCommands = allCommands.filter(
42
42
  (command) =>
43
43
  !searchQuery ||
44
- command.name.toLowerCase().includes(searchQuery.toLowerCase()),
44
+ command.id.toLowerCase().includes(searchQuery.toLowerCase()),
45
45
  );
46
46
 
47
47
  useInput((input, key) => {
@@ -50,7 +50,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
50
50
  filteredCommands.length > 0 &&
51
51
  selectedIndex < filteredCommands.length
52
52
  ) {
53
- const selectedCommand = filteredCommands[selectedIndex].name;
53
+ const selectedCommand = filteredCommands[selectedIndex].id;
54
54
  onSelect(selectedCommand);
55
55
  }
56
56
  return;
@@ -61,7 +61,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
61
61
  filteredCommands.length > 0 &&
62
62
  selectedIndex < filteredCommands.length
63
63
  ) {
64
- const selectedCommand = filteredCommands[selectedIndex].name;
64
+ const selectedCommand = filteredCommands[selectedIndex].id;
65
65
  onInsert(selectedCommand);
66
66
  }
67
67
  return;
@@ -116,12 +116,12 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
116
116
  </Box>
117
117
 
118
118
  {filteredCommands.map((command, index) => (
119
- <Box key={command.name} flexDirection="column">
119
+ <Box key={command.id} flexDirection="column">
120
120
  <Text
121
121
  color={index === selectedIndex ? "black" : "white"}
122
122
  backgroundColor={index === selectedIndex ? "magenta" : undefined}
123
123
  >
124
- {index === selectedIndex ? "▶ " : " "}/{command.name}
124
+ {index === selectedIndex ? "▶ " : " "}/{command.id}
125
125
  </Text>
126
126
  {index === selectedIndex && (
127
127
  <Box marginLeft={4}>
@@ -5,7 +5,7 @@ import type { DiffBlock } from "wave-agent-sdk";
5
5
 
6
6
  interface DiffViewerProps {
7
7
  block: DiffBlock;
8
- isExpanded?: boolean;
8
+ isStatic?: boolean;
9
9
  }
10
10
 
11
11
  // Render word-level diff
@@ -48,7 +48,7 @@ const renderWordLevelDiff = (removedLine: string, addedLine: string) => {
48
48
 
49
49
  export const DiffViewer: React.FC<DiffViewerProps> = ({
50
50
  block,
51
- isExpanded = false,
51
+ isStatic = true,
52
52
  }) => {
53
53
  const { diffResult } = block;
54
54
 
@@ -246,21 +246,22 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
246
246
  // Handle remaining deleted lines at the end
247
247
  flushPendingLines();
248
248
 
249
- // Only limit displayed lines in collapsed state
250
- if (!isExpanded) {
251
- const MAX_DISPLAY_LINES = 50;
252
- if (lines.length > MAX_DISPLAY_LINES) {
253
- const truncatedLines = lines.slice(0, MAX_DISPLAY_LINES);
254
- truncatedLines.push({
255
- content: `... (${lines.length - MAX_DISPLAY_LINES} more lines truncated, press Ctrl+O to expand)`,
256
- type: "separator",
257
- });
258
- return truncatedLines;
259
- }
249
+ return lines;
250
+ }, [diffResult]);
251
+
252
+ // Truncate to last 10 lines for non-static items
253
+ const displayLines = useMemo(() => {
254
+ if (isStatic) {
255
+ return diffLines;
260
256
  }
261
257
 
262
- return lines;
263
- }, [diffResult, isExpanded]);
258
+ const MAX_LINES = 10;
259
+ if (diffLines.length <= MAX_LINES) {
260
+ return diffLines;
261
+ }
262
+
263
+ return diffLines.slice(-MAX_LINES);
264
+ }, [diffLines, isStatic]);
264
265
 
265
266
  if (!diffResult || diffResult.length === 0) {
266
267
  return (
@@ -270,11 +271,12 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
270
271
  );
271
272
  }
272
273
 
274
+ // Show traditional diff view
273
275
  return (
274
276
  <Box flexDirection="column">
275
277
  <Box flexDirection="column">
276
278
  <Box flexDirection="column">
277
- {diffLines.map((line, index) => {
279
+ {displayLines.map((line, index) => {
278
280
  // If has word-level diff, render special effects
279
281
  if (line.wordDiff) {
280
282
  const prefix = line.type === "removed" ? "- " : "+ ";
@@ -22,7 +22,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
22
22
  const [selectedIndex, setSelectedIndex] = useState(0);
23
23
 
24
24
  useInput((input, key) => {
25
- if (key.return) {
25
+ if (key.return || key.tab) {
26
26
  if (files.length > 0 && selectedIndex < files.length) {
27
27
  onSelect(files[selectedIndex].path);
28
28
  }
@@ -126,7 +126,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
126
126
 
127
127
  <Box marginTop={1}>
128
128
  <Text dimColor>
129
- Use ↑↓ to navigate, Enter to select, Escape to cancel
129
+ Use ↑↓ to navigate, Enter/Tab to select, Escape to cancel
130
130
  </Text>
131
131
  <Text dimColor>
132
132
  File {selectedIndex + 1} of {files.length}
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect } from "react";
1
+ import React, { useEffect, useMemo } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { useInput } from "ink";
4
4
  import { FileSelector } from "./FileSelector.js";
@@ -53,19 +53,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
53
53
  slashCommands = [],
54
54
  hasSlashCommand = () => false,
55
55
  }) => {
56
- // Get current working directory
57
- const currentWorkdir = workdir || process.cwd();
58
-
59
- // Simple history navigation reset function
60
- const resetHistoryNavigation = useCallback(() => {
61
- // This will be handled by InputManager through callbacks
62
- }, []);
56
+ // Get current working directory - memoized to avoid repeated process.cwd() calls
57
+ const currentWorkdir = useMemo(() => workdir || process.cwd(), [workdir]);
63
58
 
64
59
  // Input manager with all input state and functionality (including images)
65
60
  const {
66
61
  inputText,
67
62
  cursorPosition,
68
- clearInput,
69
63
  // Image management
70
64
  attachedImages,
71
65
  clearImages,
@@ -73,24 +67,23 @@ export const InputBox: React.FC<InputBoxProps> = ({
73
67
  showFileSelector,
74
68
  filteredFiles,
75
69
  fileSearchQuery: searchQuery,
76
- handleFileSelect: handleFileSelectorSelect,
70
+ handleFileSelect,
77
71
  handleCancelFileSelect,
78
72
  // Command selector
79
73
  showCommandSelector,
80
74
  commandSearchQuery,
81
- handleCommandSelect: handleCommandSelectorSelect,
82
- handleCommandInsert: handleCommandSelectorInsert,
75
+ handleCommandSelect,
76
+ handleCommandInsert,
83
77
  handleCancelCommandSelect,
84
78
  // Bash history selector
85
79
  showBashHistorySelector,
86
80
  bashHistorySearchQuery,
87
- handleBashHistorySelect: handleBashHistorySelectorSelect,
88
- handleBashHistoryExecute,
81
+ handleBashHistorySelect,
89
82
  handleCancelBashHistorySelect,
90
83
  // Memory type selector
91
84
  showMemoryTypeSelector,
92
85
  memoryMessage,
93
- handleMemoryTypeSelect: handleMemoryTypeSelectorSelect,
86
+ handleMemoryTypeSelect,
94
87
  handleCancelMemoryTypeSelect,
95
88
  // Bash/MCP Manager
96
89
  showBashManager,
@@ -99,16 +92,17 @@ export const InputBox: React.FC<InputBoxProps> = ({
99
92
  setShowMcpManager,
100
93
  // Input history
101
94
  setUserInputHistory,
95
+ // Complex handlers combining multiple operations
96
+ handleBashHistoryExecuteAndSend,
102
97
  // Main handler
103
98
  handleInput,
99
+ // Manager ready state
100
+ isManagerReady,
104
101
  } = useInputManager({
105
- onShowBashManager: () => setShowBashManager(true),
106
- onShowMcpManager: () => setShowMcpManager(true),
107
102
  onSendMessage: sendMessage,
108
103
  onHasSlashCommand: hasSlashCommand,
109
104
  onSaveMemory: saveMemory,
110
105
  onAbortMessage: abortMessage,
111
- onResetHistoryNavigation: resetHistoryNavigation,
112
106
  });
113
107
 
114
108
  // Set user input history when it changes
@@ -128,65 +122,14 @@ export const InputBox: React.FC<InputBoxProps> = ({
128
122
  );
129
123
  });
130
124
 
131
- // Handler functions for keyboard events
132
- const handleFileSelect = useCallback(
133
- (filePath: string) => {
134
- handleFileSelectorSelect(filePath);
135
- },
136
- [handleFileSelectorSelect],
137
- );
138
-
139
- const handleCommandSelect = useCallback(
140
- (command: string) => {
141
- handleCommandSelectorSelect(command);
142
- },
143
- [handleCommandSelectorSelect],
144
- );
125
+ // These methods are already memoized in useInputManager, no need to wrap again
145
126
 
146
- const handleBashHistorySelect = useCallback(
147
- (command: string) => {
148
- handleBashHistorySelectorSelect(command);
149
- },
150
- [handleBashHistorySelectorSelect],
151
- );
152
-
153
- const keyboardHandleBashHistoryExecute = useCallback(
154
- (command: string) => {
155
- const commandToExecute = handleBashHistoryExecute(command);
156
- // Clear input box and execute command, ensure command starts with !
157
- const bashCommand = commandToExecute.startsWith("!")
158
- ? commandToExecute
159
- : `!${commandToExecute}`;
160
- clearInput();
161
- sendMessage(bashCommand);
162
- },
163
- [handleBashHistoryExecute, clearInput, sendMessage],
164
- );
165
-
166
- const handleMemoryTypeSelect = useCallback(
167
- async (type: "project" | "user") => {
168
- const currentMessage = inputText.trim();
169
- if (currentMessage.startsWith("#")) {
170
- await saveMemory(currentMessage, type);
171
- }
172
- // Call the handler function to close the selector
173
- handleMemoryTypeSelectorSelect(type);
174
- // Clear input box
175
- clearInput();
176
- },
177
- [inputText, saveMemory, handleMemoryTypeSelectorSelect, clearInput],
178
- );
127
+ // These methods are already memoized in useInputManager and combine multiple operations
179
128
 
180
129
  const isPlaceholder = !inputText;
181
130
  const placeholderText = INPUT_PLACEHOLDER_TEXT;
182
131
 
183
- // Create adapter function for CommandSelector
184
- const handleCommandInsert = useCallback(
185
- (command: string) => {
186
- handleCommandSelectorInsert(command);
187
- },
188
- [handleCommandSelectorInsert],
189
- );
132
+ // handleCommandSelectorInsert is already memoized in useInputManager, no need to wrap again
190
133
 
191
134
  // Split text into three parts: before cursor, cursor position, after cursor
192
135
  const displayText = isPlaceholder ? placeholderText : inputText;
@@ -198,8 +141,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
198
141
  // Always show cursor, allow user to continue input during loading
199
142
  const shouldShowCursor = true;
200
143
 
144
+ // Only show the Box after InputManager is created on first mount
145
+ if (!isManagerReady) {
146
+ return null;
147
+ }
148
+
201
149
  return (
202
- <Box flexDirection="column" width={"100%"}>
150
+ <Box flexDirection="column">
203
151
  {showFileSelector && (
204
152
  <FileSelector
205
153
  files={filteredFiles}
@@ -224,7 +172,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
224
172
  searchQuery={bashHistorySearchQuery}
225
173
  workdir={currentWorkdir}
226
174
  onSelect={handleBashHistorySelect}
227
- onExecute={keyboardHandleBashHistoryExecute}
175
+ onExecute={handleBashHistoryExecuteAndSend}
228
176
  onCancel={handleCancelBashHistorySelect}
229
177
  />
230
178
  )}
@@ -0,0 +1,29 @@
1
+ import React, { useMemo } from "react";
2
+ import { Text } from "ink";
3
+ import { marked } from "marked";
4
+ import TerminalRenderer from "marked-terminal";
5
+
6
+ export interface MarkdownProps {
7
+ children: string;
8
+ }
9
+
10
+ // Markdown component using marked-terminal with proper unescape option
11
+ export const Markdown = React.memo(({ children }: MarkdownProps) => {
12
+ const result = useMemo(() => {
13
+ // Configure marked with TerminalRenderer using default options
14
+ marked.setOptions({
15
+ renderer: new TerminalRenderer({
16
+ // Use official unescape option to handle HTML entities
17
+ unescape: true,
18
+ }),
19
+ });
20
+
21
+ const output = marked(children);
22
+ return typeof output === "string" ? output.trim() : "";
23
+ }, [children]);
24
+
25
+ return <Text>{result}</Text>;
26
+ });
27
+
28
+ // Add display name for debugging
29
+ Markdown.displayName = "Markdown";
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { Message } from "wave-agent-sdk";
4
+ import { MessageSource } from "wave-agent-sdk";
5
+ import { DiffViewer } from "./DiffViewer.js";
6
+ import { CommandOutputDisplay } from "./CommandOutputDisplay.js";
7
+ import { ToolResultDisplay } from "./ToolResultDisplay.js";
8
+ import { MemoryDisplay } from "./MemoryDisplay.js";
9
+ import { CompressDisplay } from "./CompressDisplay.js";
10
+ import { SubagentBlock } from "./SubagentBlock.js";
11
+ import { Markdown } from "./Markdown.js";
12
+
13
+ export interface MessageItemProps {
14
+ message: Message;
15
+ isExpanded: boolean;
16
+ shouldShowHeader: boolean;
17
+ isStatic?: boolean;
18
+ }
19
+
20
+ export const MessageItem = ({
21
+ message,
22
+ isExpanded,
23
+ shouldShowHeader,
24
+ isStatic = true,
25
+ }: MessageItemProps) => {
26
+ if (message.blocks.length === 0) return null;
27
+ return (
28
+ <Box flexDirection="column" gap={1} marginTop={1}>
29
+ {shouldShowHeader && (
30
+ <Box>
31
+ <Text color={message.role === "user" ? "cyan" : "green"} bold>
32
+ {message.role === "user" ? "👤 You" : "🤖 Assistant"}
33
+ </Text>
34
+ </Box>
35
+ )}
36
+
37
+ <Box flexDirection="column" gap={1}>
38
+ {message.blocks.map((block, blockIndex) => (
39
+ <Box key={blockIndex}>
40
+ {block.type === "text" && block.content.trim() && (
41
+ <Box>
42
+ {block.customCommandContent && (
43
+ <Text color="cyan" bold>
44
+ ⚡{" "}
45
+ </Text>
46
+ )}
47
+ {block.source === MessageSource.HOOK && (
48
+ <Text color="magenta" bold>
49
+ 🔗{" "}
50
+ </Text>
51
+ )}
52
+ {isStatic ? (
53
+ <Markdown>{block.content}</Markdown>
54
+ ) : (
55
+ <Text>{block.content.split("\n").slice(-10).join("\n")}</Text>
56
+ )}
57
+ </Box>
58
+ )}
59
+
60
+ {block.type === "error" && (
61
+ <Box>
62
+ <Text color="red">❌ Error: {block.content}</Text>
63
+ </Box>
64
+ )}
65
+
66
+ {block.type === "diff" && (
67
+ <DiffViewer block={block} isStatic={isStatic} />
68
+ )}
69
+
70
+ {block.type === "command_output" && (
71
+ <CommandOutputDisplay block={block} isExpanded={isExpanded} />
72
+ )}
73
+
74
+ {block.type === "tool" && (
75
+ <ToolResultDisplay block={block} isExpanded={isExpanded} />
76
+ )}
77
+
78
+ {block.type === "image" && (
79
+ <Box>
80
+ <Text color="magenta" bold>
81
+ 📷 Image
82
+ </Text>
83
+ {block.imageUrls && block.imageUrls.length > 0 && (
84
+ <Text color="gray" dimColor>
85
+ {" "}
86
+ ({block.imageUrls.length})
87
+ </Text>
88
+ )}
89
+ </Box>
90
+ )}
91
+
92
+ {block.type === "memory" && <MemoryDisplay block={block} />}
93
+
94
+ {block.type === "compress" && (
95
+ <CompressDisplay block={block} isExpanded={isExpanded} />
96
+ )}
97
+
98
+ {block.type === "subagent" && <SubagentBlock block={block} />}
99
+ </Box>
100
+ ))}
101
+ </Box>
102
+ </Box>
103
+ );
104
+ };