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
@@ -0,0 +1,31 @@
1
+ import React, { useState } from "react";
2
+ import { Box, useInput } from "ink";
3
+ import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
+ import { PluginList } from "./PluginList.js";
5
+
6
+ export const DiscoverView: React.FC = () => {
7
+ const { discoverablePlugins, actions } = usePluginManagerContext();
8
+ const [selectedIndex, setSelectedIndex] = useState(0);
9
+
10
+ useInput((input, key) => {
11
+ if (key.upArrow) {
12
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
13
+ } else if (key.downArrow) {
14
+ setSelectedIndex(
15
+ Math.min(discoverablePlugins.length - 1, selectedIndex + 1),
16
+ );
17
+ } else if (key.return) {
18
+ const plugin = discoverablePlugins[selectedIndex];
19
+ if (plugin) {
20
+ actions.setSelectedId(`${plugin.name}@${plugin.marketplace}`);
21
+ actions.setView("PLUGIN_DETAIL");
22
+ }
23
+ }
24
+ });
25
+
26
+ return (
27
+ <Box flexDirection="column">
28
+ <PluginList plugins={discoverablePlugins} selectedIndex={selectedIndex} />
29
+ </Box>
30
+ );
31
+ };
@@ -131,7 +131,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
131
131
  Use ↑↓ to navigate, Enter/Tab to select, Escape to cancel
132
132
  </Text>
133
133
  <Text dimColor>
134
- File {selectedIndex + 1} of {files.length}
134
+ , File {selectedIndex + 1} of {files.length}
135
135
  </Text>
136
136
  </Box>
137
137
  </Box>
@@ -0,0 +1,148 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { PromptHistoryManager, type PromptEntry } from "wave-agent-sdk";
4
+
5
+ export interface HistorySearchProps {
6
+ searchQuery: string;
7
+ onSelect: (prompt: string) => void;
8
+ onCancel: () => void;
9
+ }
10
+
11
+ export const HistorySearch: React.FC<HistorySearchProps> = ({
12
+ searchQuery,
13
+ onSelect,
14
+ onCancel,
15
+ }) => {
16
+ const [selectedIndex, setSelectedIndex] = useState(0);
17
+ const [entries, setEntries] = useState<PromptEntry[]>([]);
18
+
19
+ const entriesRef = React.useRef<PromptEntry[]>([]);
20
+ const selectedIndexRef = React.useRef(0);
21
+
22
+ useEffect(() => {
23
+ entriesRef.current = entries;
24
+ }, [entries]);
25
+
26
+ useEffect(() => {
27
+ selectedIndexRef.current = selectedIndex;
28
+ }, [selectedIndex]);
29
+
30
+ useEffect(() => {
31
+ const fetchHistory = async () => {
32
+ const results = await PromptHistoryManager.searchHistory(searchQuery);
33
+ const limitedResults = results.slice(0, 10);
34
+ setEntries(limitedResults); // Limit to 10 results
35
+ setSelectedIndex(0);
36
+ };
37
+ fetchHistory();
38
+ }, [searchQuery]);
39
+
40
+ useInput((input, key) => {
41
+ if (key.return) {
42
+ if (
43
+ entriesRef.current.length > 0 &&
44
+ selectedIndexRef.current < entriesRef.current.length
45
+ ) {
46
+ onSelect(entriesRef.current[selectedIndexRef.current].prompt);
47
+ }
48
+ return;
49
+ }
50
+
51
+ if (key.escape) {
52
+ onCancel();
53
+ return;
54
+ }
55
+
56
+ if (key.upArrow) {
57
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
58
+ return;
59
+ }
60
+
61
+ if (key.downArrow) {
62
+ setSelectedIndex((prev) =>
63
+ Math.min(entriesRef.current.length - 1, prev + 1),
64
+ );
65
+ return;
66
+ }
67
+ });
68
+
69
+ if (entries.length === 0) {
70
+ return (
71
+ <Box
72
+ flexDirection="column"
73
+ borderStyle="single"
74
+ borderColor="yellow"
75
+ borderBottom={false}
76
+ borderLeft={false}
77
+ borderRight={false}
78
+ paddingTop={1}
79
+ >
80
+ <Text color="yellow">
81
+ No history found {searchQuery && `for "${searchQuery}"`}
82
+ </Text>
83
+ <Text dimColor>Press Escape to cancel</Text>
84
+ </Box>
85
+ );
86
+ }
87
+
88
+ const formatTimestamp = (timestamp: number): string => {
89
+ const date = new Date(timestamp);
90
+ const now = new Date();
91
+ const diffMs = now.getTime() - date.getTime();
92
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
93
+ const diffDays = Math.floor(diffHours / 24);
94
+
95
+ if (diffDays > 0) {
96
+ return `${diffDays}d ago`;
97
+ } else if (diffHours > 0) {
98
+ return `${diffHours}h ago`;
99
+ } else {
100
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
101
+ return diffMinutes > 0 ? `${diffMinutes}m ago` : "just now";
102
+ }
103
+ };
104
+
105
+ return (
106
+ <Box
107
+ flexDirection="column"
108
+ borderStyle="single"
109
+ borderColor="blue"
110
+ borderBottom={false}
111
+ borderLeft={false}
112
+ borderRight={false}
113
+ paddingTop={1}
114
+ gap={1}
115
+ >
116
+ <Box>
117
+ <Text color="blue" bold>
118
+ Prompt History {searchQuery && `(filtering: "${searchQuery}")`}
119
+ </Text>
120
+ </Box>
121
+
122
+ {entries.map((entry, index) => (
123
+ <Box key={index} flexDirection="column">
124
+ <Text
125
+ color={index === selectedIndex ? "black" : "white"}
126
+ backgroundColor={index === selectedIndex ? "blue" : undefined}
127
+ wrap="truncate-end"
128
+ >
129
+ {entry.prompt.replace(/\n/g, " ")}
130
+ </Text>
131
+ {index === selectedIndex && (
132
+ <Box marginLeft={4}>
133
+ <Text color="gray" dimColor>
134
+ {formatTimestamp(entry.timestamp)}
135
+ </Text>
136
+ </Box>
137
+ )}
138
+ </Box>
139
+ ))}
140
+
141
+ <Box>
142
+ <Text dimColor>
143
+ Use ↑↓ to navigate, Enter to select, Escape to cancel
144
+ </Text>
145
+ </Box>
146
+ </Box>
147
+ );
148
+ };
@@ -1,19 +1,20 @@
1
- import React, { useEffect, useMemo } from "react";
1
+ import React, { useEffect } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { useInput } from "ink";
4
4
  import { FileSelector } from "./FileSelector.js";
5
5
  import { CommandSelector } from "./CommandSelector.js";
6
- import { BashHistorySelector } from "./BashHistorySelector.js";
6
+ import { HistorySearch } from "./HistorySearch.js";
7
7
  import { MemoryTypeSelector } from "./MemoryTypeSelector.js";
8
- import { BashShellManager } from "./BashShellManager.js";
8
+ import { TaskManager } from "./TaskManager.js";
9
9
  import { McpManager } from "./McpManager.js";
10
+ import { RewindCommand } from "./RewindCommand.js";
10
11
  import { useInputManager } from "../hooks/useInputManager.js";
11
12
  import { useChat } from "../contexts/useChat.js";
12
13
 
13
14
  import type { McpServerStatus, SlashCommand } from "wave-agent-sdk";
14
15
 
15
16
  export const INPUT_PLACEHOLDER_TEXT =
16
- "Type your message (use @ to reference files, / for commands, ! for bash history, # to add memory)...";
17
+ "Type your message (use @ to reference files, / for commands, # to add memory, Ctrl+R to search history)...";
17
18
 
18
19
  export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(
19
20
  0,
@@ -43,7 +44,6 @@ export interface InputBoxProps {
43
44
  export const InputBox: React.FC<InputBoxProps> = ({
44
45
  isLoading = false,
45
46
  isCommandRunning = false,
46
- workdir,
47
47
  userInputHistory = [],
48
48
  sendMessage = () => {},
49
49
  abortMessage = () => {},
@@ -54,12 +54,12 @@ export const InputBox: React.FC<InputBoxProps> = ({
54
54
  slashCommands = [],
55
55
  hasSlashCommand = () => false,
56
56
  }) => {
57
- // Get current working directory - memoized to avoid repeated process.cwd() calls
58
- const currentWorkdir = useMemo(() => workdir || process.cwd(), [workdir]);
59
-
60
57
  const {
61
58
  permissionMode: chatPermissionMode,
62
59
  setPermissionMode: setChatPermissionMode,
60
+ handleRewindSelect,
61
+ backgroundCurrentTask,
62
+ messages,
63
63
  } = useChat();
64
64
 
65
65
  // Input manager with all input state and functionality (including images)
@@ -81,29 +81,28 @@ export const InputBox: React.FC<InputBoxProps> = ({
81
81
  handleCommandSelect,
82
82
  handleCommandInsert,
83
83
  handleCancelCommandSelect,
84
- // Bash history selector
85
- showBashHistorySelector,
86
- bashHistorySearchQuery,
87
- handleBashHistorySelect,
88
- handleCancelBashHistorySelect,
84
+ handleHistorySearchSelect,
85
+ handleCancelHistorySearch,
89
86
  // Memory type selector
90
87
  showMemoryTypeSelector,
91
88
  memoryMessage,
92
89
  handleMemoryTypeSelect,
93
90
  handleCancelMemoryTypeSelect,
94
- // Bash/MCP Manager
95
- showBashManager,
91
+ // History search
92
+ showHistorySearch,
93
+ historySearchQuery,
94
+ // Task/MCP Manager
95
+ showTaskManager,
96
96
  showMcpManager,
97
- setShowBashManager,
97
+ showRewindManager,
98
+ setShowTaskManager,
98
99
  setShowMcpManager,
100
+ setShowRewindManager,
99
101
  // Permission mode
100
102
  permissionMode,
101
103
  setPermissionMode,
102
104
  // Input history
103
105
  setUserInputHistory,
104
- // Complex handlers combining multiple operations
105
- handleBashHistoryExecuteAndSend,
106
- handleBashHistoryDelete,
107
106
  // Main handler
108
107
  handleInput,
109
108
  // Manager ready state
@@ -113,6 +112,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
113
112
  onHasSlashCommand: hasSlashCommand,
114
113
  onSaveMemory: saveMemory,
115
114
  onAbortMessage: abortMessage,
115
+ onBackgroundCurrentTask: backgroundCurrentTask,
116
116
  onPermissionModeChange: setChatPermissionMode,
117
117
  });
118
118
 
@@ -138,9 +138,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
138
138
  );
139
139
  });
140
140
 
141
- // These methods are already memoized in useInputManager, no need to wrap again
142
-
143
- // These methods are already memoized in useInputManager and combine multiple operations
141
+ const handleRewindCancel = () => {
142
+ if (setShowRewindManager) {
143
+ setShowRewindManager(false);
144
+ }
145
+ };
144
146
 
145
147
  const isPlaceholder = !inputText;
146
148
  const placeholderText = INPUT_PLACEHOLDER_TEXT;
@@ -154,7 +156,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
154
156
  cursorPosition < displayText.length ? displayText[cursorPosition] : " ";
155
157
  const afterCursor = displayText.substring(cursorPosition + 1);
156
158
 
157
- // Always show cursor, allow user to continue input during loading
159
+ // Always show cursor, allow user to continue input during memory mode
158
160
  const shouldShowCursor = true;
159
161
 
160
162
  // Only show the Box after InputManager is created on first mount
@@ -162,6 +164,23 @@ export const InputBox: React.FC<InputBoxProps> = ({
162
164
  return null;
163
165
  }
164
166
 
167
+ const handleRewindSelectWithClose = async (index: number) => {
168
+ if (setShowRewindManager) {
169
+ setShowRewindManager(false);
170
+ }
171
+ await handleRewindSelect(index);
172
+ };
173
+
174
+ if (showRewindManager) {
175
+ return (
176
+ <RewindCommand
177
+ messages={messages}
178
+ onSelect={handleRewindSelectWithClose}
179
+ onCancel={handleRewindCancel}
180
+ />
181
+ );
182
+ }
183
+
165
184
  return (
166
185
  <Box flexDirection="column">
167
186
  {showFileSelector && (
@@ -183,14 +202,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
183
202
  />
184
203
  )}
185
204
 
186
- {showBashHistorySelector && (
187
- <BashHistorySelector
188
- searchQuery={bashHistorySearchQuery}
189
- workdir={currentWorkdir}
190
- onSelect={handleBashHistorySelect}
191
- onExecute={handleBashHistoryExecuteAndSend}
192
- onDelete={handleBashHistoryDelete}
193
- onCancel={handleCancelBashHistorySelect}
205
+ {showHistorySearch && (
206
+ <HistorySearch
207
+ searchQuery={historySearchQuery}
208
+ onSelect={handleHistorySearchSelect}
209
+ onCancel={handleCancelHistorySearch}
194
210
  />
195
211
  )}
196
212
 
@@ -202,8 +218,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
202
218
  />
203
219
  )}
204
220
 
205
- {showBashManager && (
206
- <BashShellManager onCancel={() => setShowBashManager(false)} />
221
+ {showTaskManager && (
222
+ <TaskManager onCancel={() => setShowTaskManager(false)} />
207
223
  )}
208
224
 
209
225
  {showMcpManager && (
@@ -214,7 +230,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
214
230
  onDisconnectServer={disconnectMcpServer}
215
231
  />
216
232
  )}
217
- {showBashManager || showMcpManager || (
233
+
234
+ {showTaskManager || showMcpManager || showRewindManager || (
218
235
  <Box flexDirection="column">
219
236
  <Box
220
237
  borderStyle="single"
@@ -0,0 +1,61 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
+
5
+ export const InstalledView: React.FC = () => {
6
+ const { installedPlugins, actions } = usePluginManagerContext();
7
+ const [selectedIndex, setSelectedIndex] = useState(0);
8
+
9
+ useInput((input, key) => {
10
+ if (key.upArrow) {
11
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
12
+ } else if (key.downArrow) {
13
+ setSelectedIndex(
14
+ Math.min(installedPlugins.length - 1, selectedIndex + 1),
15
+ );
16
+ } else if (key.return) {
17
+ const plugin = installedPlugins[selectedIndex];
18
+ if (plugin) {
19
+ actions.setSelectedId(`${plugin.name}@${plugin.marketplace}`);
20
+ actions.setView("PLUGIN_DETAIL");
21
+ }
22
+ }
23
+ });
24
+
25
+ if (installedPlugins.length === 0) {
26
+ return (
27
+ <Box padding={1}>
28
+ <Text dimColor>No plugins installed.</Text>
29
+ </Box>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <Box flexDirection="column">
35
+ {installedPlugins.map((plugin, index) => {
36
+ const isSelected = index === selectedIndex;
37
+ return (
38
+ <Box
39
+ key={`${plugin.name}@${plugin.marketplace}`}
40
+ flexDirection="column"
41
+ marginBottom={1}
42
+ >
43
+ <Box>
44
+ <Text color={isSelected ? "cyan" : undefined}>
45
+ {isSelected ? "> " : " "}
46
+ <Text bold>{plugin.name}</Text>
47
+ <Text dimColor> @{plugin.marketplace}</Text>
48
+ {plugin.scope && <Text color="gray"> ({plugin.scope})</Text>}
49
+ </Text>
50
+ </Box>
51
+ {isSelected && (
52
+ <Box marginLeft={4}>
53
+ <Text dimColor>Press Enter for actions</Text>
54
+ </Box>
55
+ )}
56
+ </Box>
57
+ );
58
+ })}
59
+ </Box>
60
+ );
61
+ };
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { Box, Text, useStdout } from "ink";
3
3
  import { marked, type Token, type Tokens } from "marked";
4
+ import { highlight } from "cli-highlight";
4
5
 
5
6
  export interface MarkdownProps {
6
7
  children: string;
@@ -12,7 +13,8 @@ const unescapeHtml = (html: string) => {
12
13
  .replace(/&lt;/g, "<")
13
14
  .replace(/&gt;/g, ">")
14
15
  .replace(/&quot;/g, '"')
15
- .replace(/&#39;/g, "'");
16
+ .replace(/&#39;/g, "'")
17
+ .replace(/&apos;/g, "'");
16
18
  };
17
19
 
18
20
  const InlineRenderer = ({ tokens }: { tokens: Token[] }) => {
@@ -53,16 +55,21 @@ const InlineRenderer = ({ tokens }: { tokens: Token[] }) => {
53
55
  {unescapeHtml((token as Tokens.Codespan).text)}
54
56
  </Text>
55
57
  );
56
- case "link":
58
+ case "link": {
59
+ const t = token as Tokens.Link;
57
60
  return (
58
- <Text key={index} color="blue" underline>
59
- {token.tokens ? (
60
- <InlineRenderer tokens={token.tokens} />
61
- ) : (
62
- unescapeHtml((token as Tokens.Link).text)
63
- )}
61
+ <Text key={index}>
62
+ <Text color="blue" underline>
63
+ {t.tokens ? (
64
+ <InlineRenderer tokens={t.tokens} />
65
+ ) : (
66
+ unescapeHtml(t.text)
67
+ )}
68
+ </Text>
69
+ <Text color="gray"> ({t.href})</Text>
64
70
  </Text>
65
71
  );
72
+ }
66
73
  case "br":
67
74
  return <Text key={index}>{"\n"}</Text>;
68
75
  case "del":
@@ -199,23 +206,29 @@ const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
199
206
  case "paragraph": {
200
207
  const t = token as Tokens.Paragraph;
201
208
  return (
202
- <Box
203
- key={index}
204
- marginBottom={1}
205
- flexDirection="row"
206
- flexWrap="wrap"
207
- >
208
- <InlineRenderer tokens={t.tokens} />
209
+ <Box key={index} marginBottom={1} flexDirection="row">
210
+ <Text>
211
+ <InlineRenderer tokens={t.tokens} />
212
+ </Text>
209
213
  </Box>
210
214
  );
211
215
  }
212
216
  case "code": {
213
217
  const t = token as Tokens.Code;
214
218
  if (t.lang !== undefined) {
215
- const lines = token.raw.replace(/\n$/, "").split("\n");
219
+ const raw = token.raw.endsWith("\n")
220
+ ? token.raw.slice(0, -1)
221
+ : token.raw;
222
+ const lines = raw.split("\n");
216
223
  const opening = lines[0];
217
224
  const closing = lines[lines.length - 1];
218
225
  const content = lines.slice(1, -1).join("\n");
226
+ const highlighted = content
227
+ ? highlight(unescapeHtml(content), {
228
+ language: t.lang,
229
+ ignoreIllegals: true,
230
+ })
231
+ : "";
219
232
  return (
220
233
  <Box
221
234
  key={index}
@@ -224,7 +237,7 @@ const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
224
237
  marginBottom={1}
225
238
  >
226
239
  <Text color="gray">{opening}</Text>
227
- {content && <Text>{content}</Text>}
240
+ {highlighted && <Text>{highlighted}</Text>}
228
241
  <Text color="gray">{closing}</Text>
229
242
  </Box>
230
243
  );
@@ -236,7 +249,7 @@ const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
236
249
  paddingX={1}
237
250
  marginBottom={1}
238
251
  >
239
- <Text>{t.text}</Text>
252
+ <Text>{unescapeHtml(t.text)}</Text>
240
253
  </Box>
241
254
  );
242
255
  }
@@ -258,17 +271,20 @@ const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
258
271
  </Text>
259
272
  <Box flexDirection="column" flexGrow={1}>
260
273
  {item.tokens.map((itemToken, itemIndex) => {
261
- if (itemToken.type === "text") {
262
- const it = itemToken as Tokens.Text;
274
+ if (
275
+ itemToken.type === "text" ||
276
+ itemToken.type === "paragraph"
277
+ ) {
278
+ const it = itemToken as
279
+ | Tokens.Text
280
+ | Tokens.Paragraph;
263
281
  return (
264
- <Box
265
- key={itemIndex}
266
- flexDirection="row"
267
- flexWrap="wrap"
268
- >
269
- <InlineRenderer
270
- tokens={it.tokens || [itemToken]}
271
- />
282
+ <Box key={itemIndex} flexDirection="row">
283
+ <Text>
284
+ <InlineRenderer
285
+ tokens={it.tokens || [itemToken]}
286
+ />
287
+ </Text>
272
288
  </Box>
273
289
  );
274
290
  }
@@ -0,0 +1,39 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
+
5
+ export const MarketplaceAddForm: React.FC = () => {
6
+ const { actions } = usePluginManagerContext();
7
+ const [source, setSource] = useState("");
8
+
9
+ useInput((input, key) => {
10
+ if (key.escape) {
11
+ actions.setView("MARKETPLACES");
12
+ } else if (key.return) {
13
+ if (source.trim()) {
14
+ actions.addMarketplace(source.trim());
15
+ actions.setView("MARKETPLACES");
16
+ }
17
+ } else if (key.backspace || key.delete) {
18
+ setSource((prev) => prev.slice(0, -1));
19
+ } else if (input.length === 1) {
20
+ setSource((prev) => prev + input);
21
+ }
22
+ });
23
+
24
+ return (
25
+ <Box flexDirection="column" padding={1}>
26
+ <Text bold color="cyan">
27
+ Add Marketplace
28
+ </Text>
29
+ <Box marginTop={1}>
30
+ <Text>Source (URL or Path): </Text>
31
+ <Text color="yellow">{source}</Text>
32
+ <Text color="yellow">_</Text>
33
+ </Box>
34
+ <Box marginTop={1}>
35
+ <Text dimColor>Press Enter to add, Esc to cancel</Text>
36
+ </Box>
37
+ </Box>
38
+ );
39
+ };
@@ -0,0 +1,79 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
+
5
+ export const MarketplaceDetail: React.FC = () => {
6
+ const { state, marketplaces, actions } = usePluginManagerContext();
7
+ const [selectedActionIndex, setSelectedActionIndex] = useState(0);
8
+
9
+ const marketplace = marketplaces.find((m) => m.name === state.selectedId);
10
+
11
+ const ACTIONS = [
12
+ { id: "update", label: "Update marketplace" },
13
+ { id: "remove", label: "Remove marketplace" },
14
+ ] as const;
15
+
16
+ useInput((input, key) => {
17
+ if (key.escape) {
18
+ actions.setView("MARKETPLACES");
19
+ } else if (key.upArrow) {
20
+ setSelectedActionIndex((prev) =>
21
+ prev > 0 ? prev - 1 : ACTIONS.length - 1,
22
+ );
23
+ } else if (key.downArrow) {
24
+ setSelectedActionIndex((prev) =>
25
+ prev < ACTIONS.length - 1 ? prev + 1 : 0,
26
+ );
27
+ } else if (key.return && marketplace) {
28
+ const action = ACTIONS[selectedActionIndex].id;
29
+ if (action === "update") {
30
+ actions.updateMarketplace(marketplace.name);
31
+ } else {
32
+ actions.removeMarketplace(marketplace.name);
33
+ }
34
+ actions.setView("MARKETPLACES");
35
+ }
36
+ });
37
+
38
+ if (!marketplace) {
39
+ return (
40
+ <Box>
41
+ <Text color="red">Marketplace not found.</Text>
42
+ </Box>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <Box flexDirection="column" padding={1}>
48
+ <Box marginBottom={1}>
49
+ <Text bold color="cyan">
50
+ {marketplace.name}
51
+ </Text>
52
+ {marketplace.isBuiltin && <Text dimColor> (Built-in)</Text>}
53
+ </Box>
54
+
55
+ <Box marginBottom={1}>
56
+ <Text>Source: {JSON.stringify(marketplace.source)}</Text>
57
+ </Box>
58
+
59
+ <Box marginTop={1} flexDirection="column">
60
+ <Text bold>Marketplace Actions:</Text>
61
+ {ACTIONS.map((action, index) => (
62
+ <Text
63
+ key={action.id}
64
+ color={index === selectedActionIndex ? "yellow" : undefined}
65
+ >
66
+ {index === selectedActionIndex ? "> " : " "}
67
+ {action.label}
68
+ </Text>
69
+ ))}
70
+ <Box marginTop={1}>
71
+ <Text dimColor>Use ↑/↓ to select, Enter to confirm</Text>
72
+ </Box>
73
+ <Box marginTop={1}>
74
+ <Text dimColor>Press Esc to go back</Text>
75
+ </Box>
76
+ </Box>
77
+ </Box>
78
+ );
79
+ };