wave-code 0.2.1 → 0.5.0

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 (156) hide show
  1. package/dist/commands/plugin/disable.d.ts +2 -1
  2. package/dist/commands/plugin/disable.d.ts.map +1 -1
  3. package/dist/commands/plugin/disable.js +3 -2
  4. package/dist/commands/plugin/enable.d.ts +2 -1
  5. package/dist/commands/plugin/enable.d.ts.map +1 -1
  6. package/dist/commands/plugin/enable.js +3 -2
  7. package/dist/commands/plugin/install.d.ts +2 -1
  8. package/dist/commands/plugin/install.d.ts.map +1 -1
  9. package/dist/commands/plugin/list.d.ts.map +1 -1
  10. package/dist/commands/plugin/list.js +15 -3
  11. package/dist/commands/plugin/marketplace.d.ts +3 -0
  12. package/dist/commands/plugin/marketplace.d.ts.map +1 -1
  13. package/dist/commands/plugin/marketplace.js +15 -1
  14. package/dist/commands/plugin/uninstall.d.ts +4 -0
  15. package/dist/commands/plugin/uninstall.d.ts.map +1 -0
  16. package/dist/commands/plugin/uninstall.js +29 -0
  17. package/dist/commands/plugin/update.d.ts +4 -0
  18. package/dist/commands/plugin/update.d.ts.map +1 -0
  19. package/dist/commands/plugin/update.js +15 -0
  20. package/dist/components/ChatInterface.d.ts.map +1 -1
  21. package/dist/components/ChatInterface.js +2 -2
  22. package/dist/components/CommandSelector.d.ts.map +1 -1
  23. package/dist/components/CommandSelector.js +9 -3
  24. package/dist/components/Confirmation.d.ts.map +1 -1
  25. package/dist/components/Confirmation.js +73 -20
  26. package/dist/components/DiffDisplay.d.ts +0 -1
  27. package/dist/components/DiffDisplay.d.ts.map +1 -1
  28. package/dist/components/DiffDisplay.js +38 -59
  29. package/dist/components/DiscoverView.d.ts +3 -0
  30. package/dist/components/DiscoverView.d.ts.map +1 -0
  31. package/dist/components/DiscoverView.js +25 -0
  32. package/dist/components/FileSelector.js +1 -1
  33. package/dist/components/HistorySearch.d.ts +8 -0
  34. package/dist/components/HistorySearch.d.ts.map +1 -0
  35. package/dist/components/HistorySearch.js +67 -0
  36. package/dist/components/InputBox.d.ts +1 -1
  37. package/dist/components/InputBox.d.ts.map +1 -1
  38. package/dist/components/InputBox.js +29 -19
  39. package/dist/components/InstalledView.d.ts +3 -0
  40. package/dist/components/InstalledView.d.ts.map +1 -0
  41. package/dist/components/InstalledView.js +30 -0
  42. package/dist/components/Markdown.d.ts.map +1 -1
  43. package/dist/components/Markdown.js +24 -10
  44. package/dist/components/MarketplaceAddForm.d.ts +3 -0
  45. package/dist/components/MarketplaceAddForm.d.ts.map +1 -0
  46. package/dist/components/MarketplaceAddForm.js +26 -0
  47. package/dist/components/MarketplaceDetail.d.ts +3 -0
  48. package/dist/components/MarketplaceDetail.d.ts.map +1 -0
  49. package/dist/components/MarketplaceDetail.js +38 -0
  50. package/dist/components/MarketplaceList.d.ts +9 -0
  51. package/dist/components/MarketplaceList.d.ts.map +1 -0
  52. package/dist/components/MarketplaceList.js +16 -0
  53. package/dist/components/MarketplaceView.d.ts +3 -0
  54. package/dist/components/MarketplaceView.d.ts.map +1 -0
  55. package/dist/components/MarketplaceView.js +28 -0
  56. package/dist/components/PlanDisplay.d.ts.map +1 -1
  57. package/dist/components/PlanDisplay.js +2 -2
  58. package/dist/components/PluginDetail.d.ts +3 -0
  59. package/dist/components/PluginDetail.d.ts.map +1 -0
  60. package/dist/components/PluginDetail.js +63 -0
  61. package/dist/components/PluginList.d.ts +14 -0
  62. package/dist/components/PluginList.d.ts.map +1 -0
  63. package/dist/components/PluginList.js +12 -0
  64. package/dist/components/PluginManagerShell.d.ts +5 -0
  65. package/dist/components/PluginManagerShell.d.ts.map +1 -0
  66. package/dist/components/PluginManagerShell.js +89 -0
  67. package/dist/components/PluginManagerTypes.d.ts +33 -0
  68. package/dist/components/PluginManagerTypes.d.ts.map +1 -0
  69. package/dist/components/PluginManagerTypes.js +1 -0
  70. package/dist/components/RewindCommand.d.ts +9 -0
  71. package/dist/components/RewindCommand.d.ts.map +1 -0
  72. package/dist/components/RewindCommand.js +42 -0
  73. package/dist/components/SessionSelector.d.ts +11 -0
  74. package/dist/components/SessionSelector.d.ts.map +1 -0
  75. package/dist/components/SessionSelector.js +38 -0
  76. package/dist/components/SubagentBlock.d.ts.map +1 -1
  77. package/dist/components/SubagentBlock.js +24 -1
  78. package/dist/components/TaskManager.d.ts +6 -0
  79. package/dist/components/TaskManager.d.ts.map +1 -0
  80. package/dist/components/TaskManager.js +114 -0
  81. package/dist/components/ToolResultDisplay.d.ts.map +1 -1
  82. package/dist/components/ToolResultDisplay.js +2 -1
  83. package/dist/contexts/PluginManagerContext.d.ts +4 -0
  84. package/dist/contexts/PluginManagerContext.d.ts.map +1 -0
  85. package/dist/contexts/PluginManagerContext.js +9 -0
  86. package/dist/contexts/useChat.d.ts +7 -4
  87. package/dist/contexts/useChat.d.ts.map +1 -1
  88. package/dist/contexts/useChat.js +37 -12
  89. package/dist/hooks/useInputManager.d.ts +8 -16
  90. package/dist/hooks/useInputManager.d.ts.map +1 -1
  91. package/dist/hooks/useInputManager.js +39 -55
  92. package/dist/hooks/usePluginManager.d.ts +3 -0
  93. package/dist/hooks/usePluginManager.d.ts.map +1 -0
  94. package/dist/hooks/usePluginManager.js +227 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +150 -177
  97. package/dist/managers/InputManager.d.ts +18 -26
  98. package/dist/managers/InputManager.d.ts.map +1 -1
  99. package/dist/managers/InputManager.js +93 -119
  100. package/dist/plugin-manager-cli.d.ts +6 -0
  101. package/dist/plugin-manager-cli.d.ts.map +1 -0
  102. package/dist/plugin-manager-cli.js +12 -0
  103. package/dist/session-selector-cli.d.ts +2 -0
  104. package/dist/session-selector-cli.d.ts.map +1 -0
  105. package/dist/session-selector-cli.js +25 -0
  106. package/package.json +9 -5
  107. package/src/commands/plugin/disable.ts +7 -3
  108. package/src/commands/plugin/enable.ts +7 -3
  109. package/src/commands/plugin/install.ts +2 -1
  110. package/src/commands/plugin/list.ts +21 -3
  111. package/src/commands/plugin/marketplace.ts +17 -1
  112. package/src/commands/plugin/uninstall.ts +39 -0
  113. package/src/commands/plugin/update.ts +19 -0
  114. package/src/components/ChatInterface.tsx +2 -1
  115. package/src/components/CommandSelector.tsx +10 -3
  116. package/src/components/Confirmation.tsx +115 -25
  117. package/src/components/DiffDisplay.tsx +60 -106
  118. package/src/components/DiscoverView.tsx +31 -0
  119. package/src/components/FileSelector.tsx +1 -1
  120. package/src/components/HistorySearch.tsx +148 -0
  121. package/src/components/InputBox.tsx +51 -34
  122. package/src/components/InstalledView.tsx +61 -0
  123. package/src/components/Markdown.tsx +44 -28
  124. package/src/components/MarketplaceAddForm.tsx +39 -0
  125. package/src/components/MarketplaceDetail.tsx +79 -0
  126. package/src/components/MarketplaceList.tsx +52 -0
  127. package/src/components/MarketplaceView.tsx +43 -0
  128. package/src/components/PlanDisplay.tsx +14 -19
  129. package/src/components/PluginDetail.tsx +147 -0
  130. package/src/components/PluginList.tsx +51 -0
  131. package/src/components/PluginManagerShell.tsx +189 -0
  132. package/src/components/PluginManagerTypes.ts +47 -0
  133. package/src/components/RewindCommand.tsx +114 -0
  134. package/src/components/SessionSelector.tsx +127 -0
  135. package/src/components/SubagentBlock.tsx +34 -1
  136. package/src/components/{BashShellManager.tsx → TaskManager.tsx} +79 -75
  137. package/src/components/ToolResultDisplay.tsx +6 -2
  138. package/src/contexts/PluginManagerContext.ts +15 -0
  139. package/src/contexts/useChat.tsx +51 -20
  140. package/src/hooks/useInputManager.ts +39 -71
  141. package/src/hooks/usePluginManager.ts +302 -0
  142. package/src/index.ts +241 -280
  143. package/src/managers/InputManager.ts +113 -162
  144. package/src/plugin-manager-cli.tsx +13 -0
  145. package/src/session-selector-cli.tsx +37 -0
  146. package/dist/components/BashHistorySelector.d.ts +0 -11
  147. package/dist/components/BashHistorySelector.d.ts.map +0 -1
  148. package/dist/components/BashHistorySelector.js +0 -93
  149. package/dist/components/BashShellManager.d.ts +0 -6
  150. package/dist/components/BashShellManager.d.ts.map +0 -1
  151. package/dist/components/BashShellManager.js +0 -116
  152. package/dist/hooks/usePagination.d.ts +0 -20
  153. package/dist/hooks/usePagination.d.ts.map +0 -1
  154. package/dist/hooks/usePagination.js +0 -168
  155. package/src/components/BashHistorySelector.tsx +0 -181
  156. package/src/hooks/usePagination.ts +0 -203
@@ -3,11 +3,12 @@ import {
3
3
  ConfigurationService,
4
4
  PluginManager,
5
5
  PluginScopeManager,
6
+ Scope,
6
7
  } from "wave-agent-sdk";
7
8
 
8
9
  export async function installPluginCommand(argv: {
9
10
  plugin: string;
10
- scope?: "user" | "project" | "local";
11
+ scope?: Scope;
11
12
  }) {
12
13
  const marketplaceService = new MarketplaceService();
13
14
  const workdir = process.cwd();
@@ -1,9 +1,23 @@
1
- import { MarketplaceService, ConfigurationService } from "wave-agent-sdk";
1
+ import {
2
+ MarketplaceService,
3
+ ConfigurationService,
4
+ PluginScopeManager,
5
+ PluginManager,
6
+ } from "wave-agent-sdk";
2
7
 
3
8
  export async function listPluginsCommand() {
4
- const marketplaceService = new MarketplaceService();
5
9
  const configurationService = new ConfigurationService();
10
+ const marketplaceService = new MarketplaceService();
6
11
  const workdir = process.cwd();
12
+ const pluginManager = new PluginManager({
13
+ workdir,
14
+ configurationService,
15
+ });
16
+ const pluginScopeManager = new PluginScopeManager({
17
+ workdir,
18
+ configurationService,
19
+ pluginManager,
20
+ });
7
21
 
8
22
  try {
9
23
  const installedPlugins = await marketplaceService.getInstalledPlugins();
@@ -16,6 +30,7 @@ export async function listPluginsCommand() {
16
30
  marketplace: string;
17
31
  installed: boolean;
18
32
  version?: string;
33
+ scope?: string;
19
34
  }[] = [];
20
35
 
21
36
  for (const m of marketplaces) {
@@ -24,6 +39,7 @@ export async function listPluginsCommand() {
24
39
  marketplaceService.getMarketplacePath(m),
25
40
  );
26
41
  manifest.plugins.forEach((p) => {
42
+ const pluginId = `${p.name}@${m.name}`;
27
43
  const installed = installedPlugins.plugins.find(
28
44
  (ip) => ip.name === p.name && ip.marketplace === m.name,
29
45
  );
@@ -32,6 +48,7 @@ export async function listPluginsCommand() {
32
48
  marketplace: m.name,
33
49
  installed: !!installed,
34
50
  version: installed?.version,
51
+ scope: pluginScopeManager.findPluginScope(pluginId) || undefined,
35
52
  });
36
53
  });
37
54
  } catch {
@@ -52,7 +69,8 @@ export async function listPluginsCommand() {
52
69
  : "disabled"
53
70
  : "not installed";
54
71
  const versionStr = p.version ? ` v${p.version}` : "";
55
- console.log(`- ${pluginId}${versionStr} [${status}]`);
72
+ const scopeStr = p.scope ? ` (${p.scope})` : "";
73
+ console.log(`- ${pluginId}${versionStr}${scopeStr} [${status}]`);
56
74
  });
57
75
  }
58
76
  process.exit(0);
@@ -42,7 +42,10 @@ export async function listMarketplacesCommand() {
42
42
  } else {
43
43
  sourceInfo = source.url + (source.ref ? `#${source.ref}` : "");
44
44
  }
45
- console.log(`- ${m.name}: ${sourceInfo} (${m.source.source})`);
45
+ const builtinLabel = m.isBuiltin ? " [builtin]" : "";
46
+ console.log(
47
+ `- ${m.name}${builtinLabel}: ${sourceInfo} (${m.source.source})`,
48
+ );
46
49
  });
47
50
  }
48
51
  process.exit(0);
@@ -53,6 +56,19 @@ export async function listMarketplacesCommand() {
53
56
  }
54
57
  }
55
58
 
59
+ export async function removeMarketplaceCommand(argv: { name: string }) {
60
+ const service = new MarketplaceService();
61
+ try {
62
+ await service.removeMarketplace(argv.name);
63
+ console.log(`Successfully removed marketplace: ${argv.name}`);
64
+ process.exit(0);
65
+ } catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ console.error(`Failed to remove marketplace: ${message}`);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
56
72
  export async function updateMarketplaceCommand(argv: { name?: string }) {
57
73
  const service = new MarketplaceService();
58
74
  try {
@@ -0,0 +1,39 @@
1
+ import {
2
+ MarketplaceService,
3
+ ConfigurationService,
4
+ PluginManager,
5
+ PluginScopeManager,
6
+ } from "wave-agent-sdk";
7
+
8
+ export async function uninstallPluginCommand(argv: { plugin: string }) {
9
+ const marketplaceService = new MarketplaceService();
10
+ const workdir = process.cwd();
11
+
12
+ try {
13
+ await marketplaceService.uninstallPlugin(argv.plugin, workdir);
14
+ console.log(`Successfully uninstalled plugin: ${argv.plugin}`);
15
+
16
+ const configurationService = new ConfigurationService();
17
+ const pluginManager = new PluginManager({ workdir });
18
+ const scopeManager = new PluginScopeManager({
19
+ workdir,
20
+ configurationService,
21
+ pluginManager,
22
+ });
23
+
24
+ try {
25
+ await scopeManager.removePluginFromAllScopes(argv.plugin);
26
+ console.log(`Cleaned up plugin configuration from all scopes`);
27
+ } catch (error) {
28
+ console.warn(
29
+ `Warning: Could not clean up all plugin configurations: ${error instanceof Error ? error.message : String(error)}`,
30
+ );
31
+ }
32
+
33
+ process.exit(0);
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ console.error(`Failed to uninstall plugin: ${message}`);
37
+ process.exit(1);
38
+ }
39
+ }
@@ -0,0 +1,19 @@
1
+ import { MarketplaceService } from "wave-agent-sdk";
2
+
3
+ export async function updatePluginCommand(argv: { plugin: string }) {
4
+ const marketplaceService = new MarketplaceService();
5
+
6
+ try {
7
+ const updated = await marketplaceService.updatePlugin(argv.plugin);
8
+ console.log(
9
+ `Successfully updated plugin: ${updated.name} v${updated.version} from ${updated.marketplace}`,
10
+ );
11
+ console.log(`Cache path: ${updated.cachePath}`);
12
+
13
+ process.exit(0);
14
+ } catch (error) {
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ console.error(`Failed to update plugin: ${message}`);
17
+ process.exit(1);
18
+ }
19
+ }
@@ -27,6 +27,7 @@ export const ChatInterface: React.FC = () => {
27
27
  confirmingTool,
28
28
  handleConfirmationDecision,
29
29
  handleConfirmationCancel,
30
+ rewindId,
30
31
  } = useChat();
31
32
 
32
33
  if (!sessionId) return null;
@@ -40,7 +41,7 @@ export const ChatInterface: React.FC = () => {
40
41
  isCompressing={isCompressing}
41
42
  latestTotalTokens={latestTotalTokens}
42
43
  isExpanded={isExpanded}
43
- key={String(isExpanded) + sessionId}
44
+ key={String(isExpanded) + sessionId + rewindId}
44
45
  />
45
46
 
46
47
  {isConfirmationVisible && (
@@ -4,9 +4,9 @@ import type { SlashCommand } from "wave-agent-sdk";
4
4
 
5
5
  const AVAILABLE_COMMANDS: SlashCommand[] = [
6
6
  {
7
- id: "bashes",
8
- name: "bashes",
9
- description: "View and manage background bash shells",
7
+ id: "tasks",
8
+ name: "tasks",
9
+ description: "View and manage background tasks (shells and subagents)",
10
10
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
11
11
  },
12
12
  {
@@ -15,6 +15,13 @@ const AVAILABLE_COMMANDS: SlashCommand[] = [
15
15
  description: "View and manage MCP servers",
16
16
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
17
17
  },
18
+ {
19
+ id: "rewind",
20
+ name: "rewind",
21
+ description:
22
+ "Revert conversation and file changes to a previous checkpoint",
23
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
24
+ },
18
25
  ];
19
26
 
20
27
  export interface CommandSelectorProps {
@@ -65,6 +65,7 @@ export interface ConfirmationProps {
65
65
  interface ConfirmationState {
66
66
  selectedOption: "allow" | "auto" | "alternative";
67
67
  alternativeText: string;
68
+ alternativeCursorPosition: number;
68
69
  hasUserInput: boolean; // to hide placeholder
69
70
  }
70
71
 
@@ -81,6 +82,7 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
81
82
  const [state, setState] = useState<ConfirmationState>({
82
83
  selectedOption: "allow",
83
84
  alternativeText: "",
85
+ alternativeCursorPosition: 0,
84
86
  hasUserInput: false,
85
87
  });
86
88
 
@@ -92,6 +94,7 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
92
94
  >(new Set());
93
95
  const [userAnswers, setUserAnswers] = useState<Record<string, string>>({});
94
96
  const [otherText, setOtherText] = useState("");
97
+ const [otherCursorPosition, setOtherCursorPosition] = useState(0);
95
98
 
96
99
  const questions =
97
100
  (toolInput as unknown as AskUserQuestionInput)?.questions || [];
@@ -159,6 +162,7 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
159
162
  setSelectedOptionIndex(0);
160
163
  setSelectedOptionIndices(new Set());
161
164
  setOtherText("");
165
+ setOtherCursorPosition(0);
162
166
  } else {
163
167
  // All questions answered
164
168
  onDecision({
@@ -207,10 +211,38 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
207
211
  }
208
212
 
209
213
  if (isOtherFocused) {
214
+ if (key.leftArrow) {
215
+ setOtherCursorPosition((prev) => Math.max(0, prev - 1));
216
+ return;
217
+ }
218
+ if (key.rightArrow) {
219
+ setOtherCursorPosition((prev) =>
220
+ Math.min(otherText.length, prev + 1),
221
+ );
222
+ return;
223
+ }
210
224
  if (key.backspace || key.delete) {
211
- setOtherText((prev) => prev.slice(0, -1));
212
- } else if (input && !key.ctrl && !key.meta) {
213
- setOtherText((prev) => prev + input);
225
+ if (otherCursorPosition > 0) {
226
+ setOtherText((prev) => {
227
+ const next =
228
+ prev.slice(0, otherCursorPosition - 1) +
229
+ prev.slice(otherCursorPosition);
230
+ return next;
231
+ });
232
+ setOtherCursorPosition((prev) => prev - 1);
233
+ }
234
+ return;
235
+ }
236
+ if (input && !key.ctrl && !key.meta) {
237
+ setOtherText((prev) => {
238
+ const next =
239
+ prev.slice(0, otherCursorPosition) +
240
+ input +
241
+ prev.slice(otherCursorPosition);
242
+ return next;
243
+ });
244
+ setOtherCursorPosition((prev) => prev + input.length);
245
+ return;
214
246
  }
215
247
  return;
216
248
  }
@@ -229,7 +261,7 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
229
261
  } else if (state.selectedOption === "auto") {
230
262
  if (toolName === BASH_TOOL_NAME) {
231
263
  const rule = suggestedPrefix
232
- ? `Bash(${suggestedPrefix}:*)`
264
+ ? `Bash(${suggestedPrefix}*)`
233
265
  : `Bash(${toolInput?.command})`;
234
266
  onDecision({
235
267
  behavior: "allow",
@@ -253,6 +285,29 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
253
285
  return;
254
286
  }
255
287
 
288
+ if (state.selectedOption === "alternative") {
289
+ if (key.leftArrow) {
290
+ setState((prev) => ({
291
+ ...prev,
292
+ alternativeCursorPosition: Math.max(
293
+ 0,
294
+ prev.alternativeCursorPosition - 1,
295
+ ),
296
+ }));
297
+ return;
298
+ }
299
+ if (key.rightArrow) {
300
+ setState((prev) => ({
301
+ ...prev,
302
+ alternativeCursorPosition: Math.min(
303
+ prev.alternativeText.length,
304
+ prev.alternativeCursorPosition + 1,
305
+ ),
306
+ }));
307
+ return;
308
+ }
309
+ }
310
+
256
311
  // Handle arrow keys for navigation
257
312
  if (key.upArrow) {
258
313
  setState((prev) => {
@@ -287,27 +342,42 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
287
342
  // Handle text input for alternative option
288
343
  if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
289
344
  // Focus on alternative option when user starts typing
290
- setState((prev) => ({
291
- selectedOption: "alternative",
292
- alternativeText: prev.alternativeText + input,
293
- hasUserInput: true,
294
- }));
295
- return;
296
- }
297
-
298
- // Handle backspace and delete (same behavior - delete one character)
299
- if (key.backspace || key.delete) {
300
345
  setState((prev) => {
301
- const newText = prev.alternativeText.slice(0, -1);
346
+ const nextText =
347
+ prev.alternativeText.slice(0, prev.alternativeCursorPosition) +
348
+ input +
349
+ prev.alternativeText.slice(prev.alternativeCursorPosition);
302
350
  return {
303
351
  ...prev,
304
352
  selectedOption: "alternative",
305
- alternativeText: newText,
306
- hasUserInput: newText.length > 0,
353
+ alternativeText: nextText,
354
+ alternativeCursorPosition:
355
+ prev.alternativeCursorPosition + input.length,
356
+ hasUserInput: true,
307
357
  };
308
358
  });
309
359
  return;
310
360
  }
361
+
362
+ // Handle backspace and delete
363
+ if (key.backspace || key.delete) {
364
+ setState((prev) => {
365
+ if (prev.alternativeCursorPosition > 0) {
366
+ const nextText =
367
+ prev.alternativeText.slice(0, prev.alternativeCursorPosition - 1) +
368
+ prev.alternativeText.slice(prev.alternativeCursorPosition);
369
+ return {
370
+ ...prev,
371
+ selectedOption: "alternative",
372
+ alternativeText: nextText,
373
+ alternativeCursorPosition: prev.alternativeCursorPosition - 1,
374
+ hasUserInput: nextText.length > 0,
375
+ };
376
+ }
377
+ return prev;
378
+ });
379
+ return;
380
+ }
311
381
  });
312
382
 
313
383
  const placeholderText = "Type here to tell Wave what to do differently";
@@ -329,11 +399,7 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
329
399
  </Text>
330
400
  <Text color="yellow">{getActionDescription(toolName, toolInput)}</Text>
331
401
 
332
- <DiffDisplay
333
- toolName={toolName}
334
- parameters={JSON.stringify(toolInput)}
335
- isExpanded={isExpanded}
336
- />
402
+ <DiffDisplay toolName={toolName} parameters={JSON.stringify(toolInput)} />
337
403
 
338
404
  {toolName === ASK_USER_QUESTION_TOOL_NAME &&
339
405
  currentQuestion &&
@@ -383,7 +449,15 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
383
449
  {isOther && isSelected && (
384
450
  <Text>
385
451
  :{" "}
386
- {otherText || (
452
+ {otherText ? (
453
+ <>
454
+ {otherText.slice(0, otherCursorPosition)}
455
+ <Text backgroundColor="white" color="black">
456
+ {otherText[otherCursorPosition] || " "}
457
+ </Text>
458
+ {otherText.slice(otherCursorPosition + 1)}
459
+ </>
460
+ ) : (
387
461
  <Text color="gray" dimColor>
388
462
  [Type your answer...]
389
463
  </Text>
@@ -474,8 +548,24 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
474
548
  </Text>
475
549
  ) : (
476
550
  <Text>
477
- {state.alternativeText ||
478
- "Type here to tell Wave what to do differently"}
551
+ {state.alternativeText ? (
552
+ <>
553
+ {state.alternativeText.slice(
554
+ 0,
555
+ state.alternativeCursorPosition,
556
+ )}
557
+ <Text backgroundColor="white" color="black">
558
+ {state.alternativeText[
559
+ state.alternativeCursorPosition
560
+ ] || " "}
561
+ </Text>
562
+ {state.alternativeText.slice(
563
+ state.alternativeCursorPosition + 1,
564
+ )}
565
+ </>
566
+ ) : (
567
+ "Type here to tell Wave what to do differently"
568
+ )}
479
569
  </Text>
480
570
  )}
481
571
  </Text>
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo } from "react";
2
- import { Box, Text, useStdout } from "ink";
2
+ import { Box, Text } from "ink";
3
3
  import {
4
4
  WRITE_TOOL_NAME,
5
5
  EDIT_TOOL_NAME,
@@ -11,19 +11,12 @@ import { diffLines, diffWords } from "diff";
11
11
  interface DiffDisplayProps {
12
12
  toolName?: string;
13
13
  parameters?: string;
14
- isExpanded?: boolean;
15
14
  }
16
15
 
17
16
  export const DiffDisplay: React.FC<DiffDisplayProps> = ({
18
17
  toolName,
19
18
  parameters,
20
- isExpanded = false,
21
19
  }) => {
22
- const { stdout } = useStdout();
23
- const maxHeight = useMemo(() => {
24
- return Math.max(5, (stdout?.rows || 24) - 20);
25
- }, [stdout?.rows]);
26
-
27
20
  const showDiff =
28
21
  toolName &&
29
22
  [WRITE_TOOL_NAME, EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME].includes(toolName);
@@ -107,7 +100,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
107
100
  }
108
101
  };
109
102
 
110
- // Render expanded diff display using word-level diff for all changes
103
+ // Render expanded diff display
111
104
  const renderExpandedDiff = () => {
112
105
  try {
113
106
  if (changes.length === 0) return null;
@@ -122,9 +115,8 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
122
115
  change.newContent || "",
123
116
  );
124
117
 
118
+ // Process line diffs
125
119
  const diffElements: React.ReactNode[] = [];
126
-
127
- // Process line diffs and apply word-level diff to changed lines
128
120
  lineDiffs.forEach((part, partIndex) => {
129
121
  if (part.added) {
130
122
  const lines = part.value
@@ -175,70 +167,46 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
175
167
  }
176
168
  });
177
169
 
178
- // Now look for pairs of removed/added lines that can be word-diffed
179
- let i = 0;
180
-
181
- while (i < diffElements.length) {
182
- const current = diffElements[i];
183
- const next =
184
- i + 1 < diffElements.length ? diffElements[i + 1] : null;
185
-
186
- // Check if we have a removed line followed by an added line
187
- const currentKey = React.isValidElement(current) ? current.key : "";
188
- const nextKey = React.isValidElement(next) ? next.key : "";
189
-
190
- const isCurrentRemoved =
191
- typeof currentKey === "string" && currentKey.includes("remove-");
192
- const isNextAdded =
193
- typeof nextKey === "string" && nextKey.includes("add-");
170
+ // If it's a single line change (one removed, one added), use word-level diff
171
+ if (
172
+ diffElements.length === 2 &&
173
+ React.isValidElement(diffElements[0]) &&
174
+ React.isValidElement(diffElements[1]) &&
175
+ typeof diffElements[0].key === "string" &&
176
+ diffElements[0].key.includes("remove-") &&
177
+ typeof diffElements[1].key === "string" &&
178
+ diffElements[1].key.includes("add-")
179
+ ) {
180
+ const removedText = extractTextFromElement(diffElements[0]);
181
+ const addedText = extractTextFromElement(diffElements[1]);
194
182
 
195
- if (
196
- isCurrentRemoved &&
197
- isNextAdded &&
198
- React.isValidElement(current) &&
199
- React.isValidElement(next)
200
- ) {
201
- // Extract the text content from the removed and added lines
202
- const removedText = extractTextFromElement(current);
203
- const addedText = extractTextFromElement(next);
204
-
205
- if (removedText && addedText) {
206
- // Apply word-level diff
207
- const { removedParts, addedParts } = renderWordLevelDiff(
208
- removedText,
209
- addedText,
210
- `word-${changeIndex}-${i}`,
211
- );
212
-
213
- allElements.push(
214
- <Box
215
- key={`word-diff-removed-${changeIndex}-${i}`}
216
- flexDirection="row"
217
- >
218
- <Text color="red">-</Text>
219
- {removedParts}
220
- </Box>,
221
- );
222
- allElements.push(
223
- <Box
224
- key={`word-diff-added-${changeIndex}-${i}`}
225
- flexDirection="row"
226
- >
227
- <Text color="green">+</Text>
228
- {addedParts}
229
- </Box>,
230
- );
183
+ if (removedText && addedText) {
184
+ const { removedParts, addedParts } = renderWordLevelDiff(
185
+ removedText,
186
+ addedText,
187
+ `word-${changeIndex}`,
188
+ );
231
189
 
232
- i += 2; // Skip the next element since we processed it
233
- } else {
234
- // Fallback to original elements
235
- allElements.push(current);
236
- i += 1;
237
- }
190
+ allElements.push(
191
+ <Box
192
+ key={`word-diff-removed-${changeIndex}`}
193
+ flexDirection="row"
194
+ >
195
+ <Text color="red">-</Text>
196
+ {removedParts}
197
+ </Box>,
198
+ );
199
+ allElements.push(
200
+ <Box key={`word-diff-added-${changeIndex}`} flexDirection="row">
201
+ <Text color="green">+</Text>
202
+ {addedParts}
203
+ </Box>,
204
+ );
238
205
  } else {
239
- allElements.push(current);
240
- i += 1;
206
+ allElements.push(...diffElements);
241
207
  }
208
+ } else {
209
+ allElements.push(...diffElements);
242
210
  }
243
211
  } catch (error) {
244
212
  console.warn(
@@ -255,21 +223,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
255
223
  }
256
224
  });
257
225
 
258
- const isTruncated = !isExpanded && allElements.length > maxHeight;
259
- const displayElements = isTruncated
260
- ? allElements.slice(0, maxHeight - 1)
261
- : allElements;
262
-
263
- return (
264
- <Box flexDirection="column">
265
- {displayElements}
266
- {isTruncated && (
267
- <Text color="yellow" dimColor>
268
- ... (truncated {allElements.length - (maxHeight - 1)} more lines)
269
- </Text>
270
- )}
271
- </Box>
272
- );
226
+ return <Box flexDirection="column">{allElements}</Box>;
273
227
  } catch (error) {
274
228
  console.warn("Error rendering expanded diff:", error);
275
229
  return (
@@ -280,26 +234,6 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
280
234
  }
281
235
  };
282
236
 
283
- // Helper function to extract text content from a React element
284
- const extractTextFromElement = (element: React.ReactNode): string | null => {
285
- if (!React.isValidElement(element)) return null;
286
-
287
- // Navigate through Box -> Text structure
288
- const children = (
289
- element.props as unknown as { children?: React.ReactNode[] }
290
- ).children;
291
- if (Array.isArray(children) && children.length >= 2) {
292
- const textElement = children[1]; // Second child should be the Text with content
293
- if (
294
- React.isValidElement(textElement) &&
295
- (textElement.props as unknown as { children?: string }).children
296
- ) {
297
- return (textElement.props as unknown as { children: string }).children;
298
- }
299
- }
300
- return null;
301
- };
302
-
303
237
  // Don't render anything if no diff should be shown
304
238
  if (!showDiff) {
305
239
  return null;
@@ -316,3 +250,23 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
316
250
  </Box>
317
251
  );
318
252
  };
253
+
254
+ // Helper function to extract text content from a React element
255
+ const extractTextFromElement = (element: React.ReactNode): string | null => {
256
+ if (!React.isValidElement(element)) return null;
257
+
258
+ // Navigate through Box -> Text structure
259
+ const children = (
260
+ element.props as unknown as { children?: React.ReactNode[] }
261
+ ).children;
262
+ if (Array.isArray(children) && children.length >= 2) {
263
+ const textElement = children[1]; // Second child should be the Text with content
264
+ if (
265
+ React.isValidElement(textElement) &&
266
+ (textElement.props as unknown as { children?: string }).children
267
+ ) {
268
+ return (textElement.props as unknown as { children: string }).children;
269
+ }
270
+ }
271
+ return null;
272
+ };