wave-code 0.6.5 → 0.7.1

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/cli.d.ts +1 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/plugin/disable.d.ts.map +1 -1
  5. package/dist/commands/plugin/disable.js +3 -10
  6. package/dist/commands/plugin/enable.d.ts.map +1 -1
  7. package/dist/commands/plugin/enable.js +3 -10
  8. package/dist/commands/plugin/install.d.ts.map +1 -1
  9. package/dist/commands/plugin/install.js +4 -11
  10. package/dist/commands/plugin/list.d.ts.map +1 -1
  11. package/dist/commands/plugin/list.js +5 -39
  12. package/dist/commands/plugin/marketplace.js +9 -9
  13. package/dist/commands/plugin/uninstall.d.ts.map +1 -1
  14. package/dist/commands/plugin/uninstall.js +4 -17
  15. package/dist/commands/plugin/update.js +3 -3
  16. package/dist/components/App.d.ts +1 -0
  17. package/dist/components/App.d.ts.map +1 -1
  18. package/dist/components/App.js +4 -4
  19. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  20. package/dist/components/BackgroundTaskManager.js +34 -18
  21. package/dist/components/ChatInterface.d.ts.map +1 -1
  22. package/dist/components/ChatInterface.js +28 -15
  23. package/dist/components/ConfirmationDetails.d.ts +1 -0
  24. package/dist/components/ConfirmationDetails.d.ts.map +1 -1
  25. package/dist/components/ConfirmationDetails.js +7 -14
  26. package/dist/components/ConfirmationSelector.d.ts +1 -0
  27. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  28. package/dist/components/ConfirmationSelector.js +164 -117
  29. package/dist/components/DiffDisplay.d.ts +1 -0
  30. package/dist/components/DiffDisplay.d.ts.map +1 -1
  31. package/dist/components/DiffDisplay.js +94 -36
  32. package/dist/components/HistorySearch.d.ts.map +1 -1
  33. package/dist/components/HistorySearch.js +26 -20
  34. package/dist/components/Markdown.d.ts.map +1 -1
  35. package/dist/components/Markdown.js +3 -1
  36. package/dist/components/McpManager.d.ts.map +1 -1
  37. package/dist/components/McpManager.js +49 -52
  38. package/dist/components/MessageBlockItem.d.ts +9 -0
  39. package/dist/components/MessageBlockItem.d.ts.map +1 -0
  40. package/dist/components/MessageBlockItem.js +11 -0
  41. package/dist/components/MessageList.d.ts +2 -4
  42. package/dist/components/MessageList.d.ts.map +1 -1
  43. package/dist/components/MessageList.js +28 -23
  44. package/dist/components/PluginDetail.d.ts.map +1 -1
  45. package/dist/components/PluginDetail.js +19 -22
  46. package/dist/components/SessionSelector.d.ts.map +1 -1
  47. package/dist/components/SessionSelector.js +8 -5
  48. package/dist/components/TaskList.d.ts.map +1 -1
  49. package/dist/components/TaskList.js +2 -5
  50. package/dist/components/ToolDisplay.d.ts.map +1 -1
  51. package/dist/components/ToolDisplay.js +1 -1
  52. package/dist/contexts/useChat.d.ts +1 -0
  53. package/dist/contexts/useChat.d.ts.map +1 -1
  54. package/dist/contexts/useChat.js +20 -3
  55. package/dist/hooks/usePluginManager.d.ts.map +1 -1
  56. package/dist/hooks/usePluginManager.js +20 -39
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +16 -0
  59. package/dist/print-cli.d.ts +1 -0
  60. package/dist/print-cli.d.ts.map +1 -1
  61. package/dist/print-cli.js +2 -1
  62. package/dist/utils/highlightUtils.d.ts +2 -0
  63. package/dist/utils/highlightUtils.d.ts.map +1 -0
  64. package/dist/utils/highlightUtils.js +69 -0
  65. package/dist/utils/toolParameterTransforms.d.ts +2 -6
  66. package/dist/utils/toolParameterTransforms.d.ts.map +1 -1
  67. package/dist/utils/toolParameterTransforms.js +10 -14
  68. package/package.json +4 -2
  69. package/src/cli.tsx +3 -0
  70. package/src/commands/plugin/disable.ts +3 -17
  71. package/src/commands/plugin/enable.ts +3 -17
  72. package/src/commands/plugin/install.ts +4 -18
  73. package/src/commands/plugin/list.ts +5 -55
  74. package/src/commands/plugin/marketplace.ts +9 -9
  75. package/src/commands/plugin/uninstall.ts +4 -26
  76. package/src/commands/plugin/update.ts +3 -3
  77. package/src/components/App.tsx +10 -2
  78. package/src/components/BackgroundTaskManager.tsx +69 -44
  79. package/src/components/ChatInterface.tsx +35 -23
  80. package/src/components/ConfirmationDetails.tsx +13 -15
  81. package/src/components/ConfirmationSelector.tsx +207 -128
  82. package/src/components/DiffDisplay.tsx +164 -75
  83. package/src/components/HistorySearch.tsx +31 -25
  84. package/src/components/Markdown.tsx +3 -1
  85. package/src/components/McpManager.tsx +51 -59
  86. package/src/components/MessageBlockItem.tsx +83 -0
  87. package/src/components/MessageList.tsx +55 -52
  88. package/src/components/PluginDetail.tsx +30 -31
  89. package/src/components/SessionSelector.tsx +8 -5
  90. package/src/components/TaskList.tsx +2 -5
  91. package/src/components/ToolDisplay.tsx +5 -1
  92. package/src/contexts/useChat.tsx +22 -2
  93. package/src/hooks/usePluginManager.ts +21 -57
  94. package/src/index.ts +17 -0
  95. package/src/print-cli.ts +3 -0
  96. package/src/utils/highlightUtils.ts +76 -0
  97. package/src/utils/toolParameterTransforms.ts +11 -20
  98. package/dist/components/MessageItem.d.ts +0 -8
  99. package/dist/components/MessageItem.d.ts.map +0 -1
  100. package/dist/components/MessageItem.js +0 -13
  101. package/src/components/MessageItem.tsx +0 -81
@@ -1,37 +1,34 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import {
4
- WRITE_TOOL_NAME,
5
- EDIT_TOOL_NAME,
6
- MULTI_EDIT_TOOL_NAME,
7
- } from "wave-agent-sdk";
3
+ import { WRITE_TOOL_NAME, EDIT_TOOL_NAME } from "wave-agent-sdk";
8
4
  import { transformToolBlockToChanges } from "../utils/toolParameterTransforms.js";
9
5
  import { diffLines, diffWords } from "diff";
10
6
 
11
7
  interface DiffDisplayProps {
12
8
  toolName?: string;
13
9
  parameters?: string;
10
+ startLineNumber?: number;
14
11
  }
15
12
 
16
13
  export const DiffDisplay: React.FC<DiffDisplayProps> = ({
17
14
  toolName,
18
15
  parameters,
16
+ startLineNumber,
19
17
  }) => {
20
18
  const showDiff =
21
- toolName &&
22
- [WRITE_TOOL_NAME, EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME].includes(toolName);
19
+ toolName && [WRITE_TOOL_NAME, EDIT_TOOL_NAME].includes(toolName);
23
20
 
24
21
  // Diff detection and transformation using typed parameters
25
22
  const changes = useMemo(() => {
26
23
  if (!showDiff || !toolName || !parameters) return [];
27
24
  try {
28
25
  // Use local transformation with JSON parsing and type guards
29
- return transformToolBlockToChanges(toolName, parameters);
26
+ return transformToolBlockToChanges(toolName, parameters, startLineNumber);
30
27
  } catch (error) {
31
28
  console.warn("Error transforming tool block to changes:", error);
32
29
  return [];
33
30
  }
34
- }, [toolName, parameters, showDiff]);
31
+ }, [toolName, parameters, showDiff, startLineNumber]);
35
32
 
36
33
  // Render word-level diff between two lines of text
37
34
  const renderWordLevelDiff = (
@@ -105,64 +102,93 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
105
102
  try {
106
103
  if (changes.length === 0) return null;
107
104
 
105
+ const maxLineNum = changes.reduce((max, change) => {
106
+ const oldLines = (change.oldContent || "").split("\n").length;
107
+ const newLines = (change.newContent || "").split("\n").length;
108
+ const start = change.startLineNumber || 1;
109
+ // For Edit tool, the diff might show context lines before/after the change.
110
+ // The startLineNumber is the line where old_string starts.
111
+ // diffLines will include context lines if they are part of the change object.
112
+ // However, our transformEditParameters currently only puts old_string/new_string.
113
+ // If we ever support context lines in the Change object, we need to be careful.
114
+ return Math.max(max, start + oldLines, start + newLines);
115
+ }, 0);
116
+ const maxDigits = Math.max(2, maxLineNum.toString().length);
117
+
118
+ const renderLine = (
119
+ oldLineNum: number | null,
120
+ newLineNum: number | null,
121
+ prefix: string,
122
+ content: React.ReactNode,
123
+ color: string,
124
+ key: string,
125
+ ) => {
126
+ const formatNum = (num: number | null) =>
127
+ num === null
128
+ ? " ".repeat(maxDigits)
129
+ : num.toString().padStart(maxDigits);
130
+
131
+ return (
132
+ <Box key={key} flexDirection="row">
133
+ <Text color="gray">{formatNum(oldLineNum)} </Text>
134
+ <Text color="gray">{formatNum(newLineNum)} </Text>
135
+ <Text color="gray">| </Text>
136
+ <Text color={color}>{prefix}</Text>
137
+ <Text color={color}>{content}</Text>
138
+ </Box>
139
+ );
140
+ };
141
+
108
142
  const allElements: React.ReactNode[] = [];
109
143
 
110
144
  changes.forEach((change, changeIndex) => {
111
145
  try {
112
- // Add ellipsis between non-contiguous edits in MultiEdit
113
- if (toolName === MULTI_EDIT_TOOL_NAME && changeIndex > 0) {
114
- allElements.push(
115
- <Box key={`multi-edit-separator-${changeIndex}`}>
116
- <Text color="gray">...</Text>
117
- </Box>,
118
- );
119
- }
120
-
121
146
  // Get line-level diff to understand the structure
122
147
  const lineDiffs = diffLines(
123
148
  change.oldContent || "",
124
149
  change.newContent || "",
125
150
  );
126
151
 
152
+ let oldLineNum = change.startLineNumber || 1;
153
+ let newLineNum = change.startLineNumber || 1;
154
+
127
155
  // Process line diffs
128
156
  const diffElements: React.ReactNode[] = [];
129
157
  lineDiffs.forEach((part, partIndex) => {
158
+ const lines = part.value.split("\n");
159
+ // diffLines might return a trailing empty string if the content ends with a newline
160
+ if (lines[lines.length - 1] === "") {
161
+ lines.pop();
162
+ }
163
+
130
164
  if (part.added) {
131
- const lines = part.value
132
- .split("\n")
133
- .filter((line) => line !== "");
134
165
  lines.forEach((line, lineIndex) => {
135
166
  diffElements.push(
136
- <Box
137
- key={`add-${changeIndex}-${partIndex}-${lineIndex}`}
138
- flexDirection="row"
139
- >
140
- <Text color="green">+</Text>
141
- <Text color="green">{line}</Text>
142
- </Box>,
167
+ renderLine(
168
+ null,
169
+ newLineNum++,
170
+ "+",
171
+ line,
172
+ "green",
173
+ `add-${changeIndex}-${partIndex}-${lineIndex}`,
174
+ ),
143
175
  );
144
176
  });
145
177
  } else if (part.removed) {
146
- const lines = part.value
147
- .split("\n")
148
- .filter((line) => line !== "");
149
178
  lines.forEach((line, lineIndex) => {
150
179
  diffElements.push(
151
- <Box
152
- key={`remove-${changeIndex}-${partIndex}-${lineIndex}`}
153
- flexDirection="row"
154
- >
155
- <Text color="red">-</Text>
156
- <Text color="red">{line}</Text>
157
- </Box>,
180
+ renderLine(
181
+ oldLineNum++,
182
+ null,
183
+ "-",
184
+ line,
185
+ "red",
186
+ `remove-${changeIndex}-${partIndex}-${lineIndex}`,
187
+ ),
158
188
  );
159
189
  });
160
190
  } else {
161
191
  // Context lines - show unchanged content
162
- const lines = part.value
163
- .split("\n")
164
- .filter((line) => line !== "");
165
-
166
192
  const isFirstBlock = partIndex === 0;
167
193
  const isLastBlock = partIndex === lineDiffs.length - 1;
168
194
 
@@ -173,6 +199,9 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
173
199
  if (isFirstBlock && !isLastBlock) {
174
200
  // First block: keep last 3
175
201
  if (lines.length > 3) {
202
+ const skipCount = lines.length - 3;
203
+ oldLineNum += skipCount;
204
+ newLineNum += skipCount;
176
205
  linesToDisplay = lines.slice(-3);
177
206
  showEllipsisTop = true;
178
207
  }
@@ -188,15 +217,12 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
188
217
  linesToDisplay = [...lines.slice(0, 3), ...lines.slice(-3)];
189
218
  showEllipsisTop = false; // We'll put ellipsis in the middle
190
219
  }
191
- } else if (isFirstBlock && isLastBlock) {
192
- // Only one block (no changes?) - keep all or apply a general limit
193
- // For now, let's keep all if it's the only block
194
220
  }
195
221
 
196
222
  if (showEllipsisTop) {
197
223
  diffElements.push(
198
224
  <Box key={`ellipsis-top-${changeIndex}-${partIndex}`}>
199
- <Text color="gray"> ...</Text>
225
+ <Text color="gray">{" ".repeat(maxDigits * 2 + 2)}...</Text>
200
226
  </Box>,
201
227
  );
202
228
  }
@@ -209,28 +235,39 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
209
235
  lines.length > 6 &&
210
236
  lineIndex === 3
211
237
  ) {
238
+ const skipCount = lines.length - 6;
239
+ oldLineNum += skipCount;
240
+ newLineNum += skipCount;
212
241
  diffElements.push(
213
242
  <Box key={`ellipsis-mid-${changeIndex}-${partIndex}`}>
214
- <Text color="gray"> ...</Text>
243
+ <Text color="gray">
244
+ {" ".repeat(maxDigits * 2 + 2)}...
245
+ </Text>
215
246
  </Box>,
216
247
  );
217
248
  }
218
249
 
219
250
  diffElements.push(
220
- <Box
221
- key={`context-${changeIndex}-${partIndex}-${lineIndex}`}
222
- flexDirection="row"
223
- >
224
- <Text color="white"> </Text>
225
- <Text color="white">{line}</Text>
226
- </Box>,
251
+ renderLine(
252
+ oldLineNum++,
253
+ newLineNum++,
254
+ " ",
255
+ line,
256
+ "white",
257
+ `context-${changeIndex}-${partIndex}-${lineIndex}`,
258
+ ),
227
259
  );
228
260
  });
229
261
 
230
262
  if (showEllipsisBottom) {
263
+ const skipCount = lines.length - linesToDisplay.length;
264
+ // We don't increment oldLineNum/newLineNum here because they are already incremented in the loop
265
+ // But we need to account for the lines we skipped at the end of this block
266
+ oldLineNum += skipCount;
267
+ newLineNum += skipCount;
231
268
  diffElements.push(
232
269
  <Box key={`ellipsis-bottom-${changeIndex}-${partIndex}`}>
233
- <Text color="gray"> ...</Text>
270
+ <Text color="gray">{" ".repeat(maxDigits * 2 + 2)}...</Text>
234
271
  </Box>,
235
272
  );
236
273
  }
@@ -249,6 +286,8 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
249
286
  ) {
250
287
  const removedText = extractTextFromElement(diffElements[0]);
251
288
  const addedText = extractTextFromElement(diffElements[1]);
289
+ const oldLineNumVal = extractOldLineNumFromElement(diffElements[0]);
290
+ const newLineNumVal = extractNewLineNumFromElement(diffElements[1]);
252
291
 
253
292
  if (removedText && addedText) {
254
293
  const { removedParts, addedParts } = renderWordLevelDiff(
@@ -258,19 +297,24 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
258
297
  );
259
298
 
260
299
  allElements.push(
261
- <Box
262
- key={`word-diff-removed-${changeIndex}`}
263
- flexDirection="row"
264
- >
265
- <Text color="red">-</Text>
266
- {removedParts}
267
- </Box>,
300
+ renderLine(
301
+ oldLineNumVal,
302
+ null,
303
+ "-",
304
+ removedParts,
305
+ "red",
306
+ `word-diff-removed-${changeIndex}`,
307
+ ),
268
308
  );
269
309
  allElements.push(
270
- <Box key={`word-diff-added-${changeIndex}`} flexDirection="row">
271
- <Text color="green">+</Text>
272
- {addedParts}
273
- </Box>,
310
+ renderLine(
311
+ null,
312
+ newLineNumVal,
313
+ "+",
314
+ addedParts,
315
+ "green",
316
+ `word-diff-added-${changeIndex}`,
317
+ ),
274
318
  );
275
319
  } else {
276
320
  allElements.push(...diffElements);
@@ -312,9 +356,6 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
312
356
  return (
313
357
  <Box flexDirection="column">
314
358
  <Box paddingLeft={2} borderLeft borderColor="cyan" flexDirection="column">
315
- <Text color="cyan" bold>
316
- Diff:
317
- </Text>
318
359
  {renderExpandedDiff()}
319
360
  </Box>
320
361
  </Box>
@@ -326,16 +367,64 @@ const extractTextFromElement = (element: React.ReactNode): string | null => {
326
367
  if (!React.isValidElement(element)) return null;
327
368
 
328
369
  // Navigate through Box -> Text structure
370
+ // Our new structure is: Box -> Text (old), Text (new), Text (|), Text (prefix), Text (content)
371
+ const children = (
372
+ element.props as unknown as { children?: React.ReactNode[] }
373
+ ).children;
374
+ if (Array.isArray(children) && children.length >= 5) {
375
+ const textElement = children[4]; // Fifth child should be the Text with content
376
+ if (React.isValidElement(textElement)) {
377
+ const textChildren = (textElement.props as Record<string, unknown>)
378
+ .children;
379
+ return Array.isArray(textChildren)
380
+ ? textChildren.join("")
381
+ : String(textChildren || "");
382
+ }
383
+ }
384
+ return null;
385
+ };
386
+
387
+ const extractOldLineNumFromElement = (
388
+ element: React.ReactNode,
389
+ ): number | null => {
390
+ if (!React.isValidElement(element)) return null;
391
+ const children = (
392
+ element.props as unknown as { children?: React.ReactNode[] }
393
+ ).children;
394
+ if (Array.isArray(children) && children.length >= 1) {
395
+ const textElement = children[0];
396
+ if (React.isValidElement(textElement)) {
397
+ const textChildren = (textElement.props as Record<string, unknown>)
398
+ .children;
399
+ const val = (
400
+ Array.isArray(textChildren)
401
+ ? textChildren.join("")
402
+ : String(textChildren || "")
403
+ ).trim();
404
+ return val ? parseInt(val, 10) : null;
405
+ }
406
+ }
407
+ return null;
408
+ };
409
+
410
+ const extractNewLineNumFromElement = (
411
+ element: React.ReactNode,
412
+ ): number | null => {
413
+ if (!React.isValidElement(element)) return null;
329
414
  const children = (
330
415
  element.props as unknown as { children?: React.ReactNode[] }
331
416
  ).children;
332
417
  if (Array.isArray(children) && children.length >= 2) {
333
- const textElement = children[1]; // Second child should be the Text with content
334
- if (
335
- React.isValidElement(textElement) &&
336
- (textElement.props as unknown as { children?: string }).children
337
- ) {
338
- return (textElement.props as unknown as { children: string }).children;
418
+ const textElement = children[1];
419
+ if (React.isValidElement(textElement)) {
420
+ const textChildren = (textElement.props as Record<string, unknown>)
421
+ .children;
422
+ const val = (
423
+ Array.isArray(textChildren)
424
+ ? textChildren.join("")
425
+ : String(textChildren || "")
426
+ ).trim();
427
+ return val ? parseInt(val, 10) : null;
339
428
  }
340
429
  }
341
430
  return null;
@@ -14,38 +14,34 @@ export const HistorySearch: React.FC<HistorySearchProps> = ({
14
14
  onCancel,
15
15
  }) => {
16
16
  const MAX_VISIBLE_ITEMS = 5;
17
- const [selectedIndex, setSelectedIndex] = useState(0);
18
- const [entries, setEntries] = useState<PromptEntry[]>([]);
19
-
20
- const entriesRef = React.useRef<PromptEntry[]>([]);
21
- const selectedIndexRef = React.useRef(0);
22
-
23
- useEffect(() => {
24
- entriesRef.current = entries;
25
- }, [entries]);
26
-
27
- useEffect(() => {
28
- selectedIndexRef.current = selectedIndex;
29
- }, [selectedIndex]);
17
+ const [state, setState] = useState({
18
+ selectedIndex: 0,
19
+ entries: [] as PromptEntry[],
20
+ });
30
21
 
31
22
  useEffect(() => {
32
23
  const fetchHistory = async () => {
33
24
  const results = await PromptHistoryManager.searchHistory(searchQuery);
34
25
  const limitedResults = results.slice(0, 20);
35
- setEntries(limitedResults); // Limit to 20 results
36
- setSelectedIndex(0);
26
+ setState({
27
+ entries: limitedResults,
28
+ selectedIndex: 0,
29
+ });
37
30
  };
38
31
  fetchHistory();
39
32
  }, [searchQuery]);
40
33
 
41
34
  useInput((input, key) => {
42
35
  if (key.return) {
43
- if (
44
- entriesRef.current.length > 0 &&
45
- selectedIndexRef.current < entriesRef.current.length
46
- ) {
47
- onSelect(entriesRef.current[selectedIndexRef.current].prompt);
48
- }
36
+ setState((prev) => {
37
+ if (
38
+ prev.entries.length > 0 &&
39
+ prev.selectedIndex < prev.entries.length
40
+ ) {
41
+ onSelect(prev.entries[prev.selectedIndex].prompt);
42
+ }
43
+ return prev;
44
+ });
49
45
  return;
50
46
  }
51
47
 
@@ -55,18 +51,27 @@ export const HistorySearch: React.FC<HistorySearchProps> = ({
55
51
  }
56
52
 
57
53
  if (key.upArrow) {
58
- setSelectedIndex((prev) => Math.max(0, prev - 1));
54
+ setState((prev) => ({
55
+ ...prev,
56
+ selectedIndex: Math.max(0, prev.selectedIndex - 1),
57
+ }));
59
58
  return;
60
59
  }
61
60
 
62
61
  if (key.downArrow) {
63
- setSelectedIndex((prev) =>
64
- Math.min(entriesRef.current.length - 1, prev + 1),
65
- );
62
+ setState((prev) => ({
63
+ ...prev,
64
+ selectedIndex: Math.min(
65
+ prev.entries.length - 1,
66
+ prev.selectedIndex + 1,
67
+ ),
68
+ }));
66
69
  return;
67
70
  }
68
71
  });
69
72
 
73
+ const { entries, selectedIndex } = state;
74
+
70
75
  if (entries.length === 0) {
71
76
  return (
72
77
  <Box
@@ -147,6 +152,7 @@ export const HistorySearch: React.FC<HistorySearchProps> = ({
147
152
  backgroundColor={isSelected ? "blue" : undefined}
148
153
  wrap="truncate-end"
149
154
  >
155
+ {isSelected ? "> " : " "}
150
156
  {entry.prompt.replace(/\n/g, " ")}
151
157
  </Text>
152
158
  </Box>
@@ -2,6 +2,7 @@ import React, { useMemo } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { Renderer, marked, type Tokens } from "marked";
4
4
  import chalk from "chalk";
5
+ import { highlightToAnsi } from "../utils/highlightUtils.js";
5
6
 
6
7
  export interface MarkdownProps {
7
8
  children: string;
@@ -21,7 +22,8 @@ class AnsiRenderer extends Renderer<string> {
21
22
  override code({ text, lang }: Tokens.Code): string {
22
23
  const prefix = lang ? `\`\`\`${lang}` : "```";
23
24
  const suffix = "```";
24
- return `\n${chalk.gray(prefix)}\n${text}\n${chalk.gray(suffix)}\n`;
25
+ const highlighted = highlightToAnsi(text, lang);
26
+ return `\n${chalk.gray(prefix)}\n${highlighted}\n${chalk.gray(suffix)}\n`;
25
27
  }
26
28
 
27
29
  override blockquote({ tokens }: Tokens.Blockquote): string {
@@ -65,76 +65,68 @@ export const McpManager: React.FC<McpManagerProps> = ({
65
65
  };
66
66
 
67
67
  useInput((input, key) => {
68
- if (viewMode === "list") {
69
- // List mode navigation
70
- if (key.return) {
71
- if (servers.length > 0 && selectedIndex < servers.length) {
72
- setViewMode("detail");
68
+ if (key.return) {
69
+ setViewMode((prevMode) => {
70
+ if (prevMode === "list") {
71
+ setSelectedIndex((prevIndex) => {
72
+ if (servers.length > 0 && prevIndex < servers.length) {
73
+ // We can't call setViewMode here because we're already in a setViewMode call
74
+ // But we can return the new mode from the outer setViewMode
75
+ }
76
+ return prevIndex;
77
+ });
78
+ return "detail";
73
79
  }
74
- return;
75
- }
80
+ return prevMode;
81
+ });
82
+ return;
83
+ }
76
84
 
77
- if (key.escape) {
85
+ if (key.escape) {
86
+ setViewMode((prev) => {
87
+ if (prev === "detail") {
88
+ return "list";
89
+ }
78
90
  onCancel();
79
- return;
80
- }
81
-
82
- if (key.upArrow) {
83
- setSelectedIndex(Math.max(0, selectedIndex - 1));
84
- return;
85
- }
86
-
87
- if (key.downArrow) {
88
- setSelectedIndex(Math.min(servers.length - 1, selectedIndex + 1));
89
- return;
90
- }
91
+ return prev;
92
+ });
93
+ return;
94
+ }
91
95
 
92
- // Hotkeys for server actions
93
- if (
94
- input === "c" &&
95
- servers.length > 0 &&
96
- selectedIndex < servers.length
97
- ) {
98
- const server = servers[selectedIndex];
99
- if (server.status === "disconnected" || server.status === "error") {
100
- handleConnect(server.name);
101
- }
102
- return;
103
- }
96
+ if (key.upArrow) {
97
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
98
+ return;
99
+ }
104
100
 
105
- if (
106
- input === "d" &&
107
- servers.length > 0 &&
108
- selectedIndex < servers.length
109
- ) {
110
- const server = servers[selectedIndex];
111
- if (server.status === "connected") {
112
- handleDisconnect(server.name);
113
- }
114
- return;
115
- }
116
- } else if (viewMode === "detail") {
117
- // Detail mode navigation
118
- if (key.escape) {
119
- setViewMode("list");
120
- return;
121
- }
101
+ if (key.downArrow) {
102
+ setSelectedIndex((prev) => Math.min(servers.length - 1, prev + 1));
103
+ return;
104
+ }
122
105
 
123
- if (selectedServer) {
106
+ // Hotkeys for server actions
107
+ if (input === "c") {
108
+ setSelectedIndex((prev) => {
109
+ const server = servers[prev];
124
110
  if (
125
- input === "c" &&
126
- (selectedServer.status === "disconnected" ||
127
- selectedServer.status === "error")
111
+ server &&
112
+ (server.status === "disconnected" || server.status === "error")
128
113
  ) {
129
- handleConnect(selectedServer.name);
130
- return;
114
+ handleConnect(server.name);
131
115
  }
116
+ return prev;
117
+ });
118
+ return;
119
+ }
132
120
 
133
- if (input === "d" && selectedServer.status === "connected") {
134
- handleDisconnect(selectedServer.name);
135
- return;
121
+ if (input === "d") {
122
+ setSelectedIndex((prev) => {
123
+ const server = servers[prev];
124
+ if (server && server.status === "connected") {
125
+ handleDisconnect(server.name);
136
126
  }
137
- }
127
+ return prev;
128
+ });
129
+ return;
138
130
  }
139
131
  });
140
132