wave-code 0.2.0 → 0.4.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 (140) 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 +6 -0
  24. package/dist/components/Confirmation.js +1 -1
  25. package/dist/components/DiscoverView.d.ts +3 -0
  26. package/dist/components/DiscoverView.d.ts.map +1 -0
  27. package/dist/components/DiscoverView.js +25 -0
  28. package/dist/components/FileSelector.js +1 -1
  29. package/dist/components/HistorySearch.d.ts +8 -0
  30. package/dist/components/HistorySearch.d.ts.map +1 -0
  31. package/dist/components/HistorySearch.js +67 -0
  32. package/dist/components/InputBox.d.ts +1 -1
  33. package/dist/components/InputBox.d.ts.map +1 -1
  34. package/dist/components/InputBox.js +26 -17
  35. package/dist/components/InstalledView.d.ts +3 -0
  36. package/dist/components/InstalledView.d.ts.map +1 -0
  37. package/dist/components/InstalledView.js +30 -0
  38. package/dist/components/Markdown.d.ts.map +1 -1
  39. package/dist/components/Markdown.js +22 -9
  40. package/dist/components/MarketplaceAddForm.d.ts +3 -0
  41. package/dist/components/MarketplaceAddForm.d.ts.map +1 -0
  42. package/dist/components/MarketplaceAddForm.js +26 -0
  43. package/dist/components/MarketplaceDetail.d.ts +3 -0
  44. package/dist/components/MarketplaceDetail.d.ts.map +1 -0
  45. package/dist/components/MarketplaceDetail.js +38 -0
  46. package/dist/components/MarketplaceList.d.ts +9 -0
  47. package/dist/components/MarketplaceList.d.ts.map +1 -0
  48. package/dist/components/MarketplaceList.js +16 -0
  49. package/dist/components/MarketplaceView.d.ts +3 -0
  50. package/dist/components/MarketplaceView.d.ts.map +1 -0
  51. package/dist/components/MarketplaceView.js +28 -0
  52. package/dist/components/PluginDetail.d.ts +3 -0
  53. package/dist/components/PluginDetail.d.ts.map +1 -0
  54. package/dist/components/PluginDetail.js +63 -0
  55. package/dist/components/PluginList.d.ts +14 -0
  56. package/dist/components/PluginList.d.ts.map +1 -0
  57. package/dist/components/PluginList.js +12 -0
  58. package/dist/components/PluginManagerShell.d.ts +5 -0
  59. package/dist/components/PluginManagerShell.d.ts.map +1 -0
  60. package/dist/components/PluginManagerShell.js +89 -0
  61. package/dist/components/PluginManagerTypes.d.ts +33 -0
  62. package/dist/components/PluginManagerTypes.d.ts.map +1 -0
  63. package/dist/components/PluginManagerTypes.js +1 -0
  64. package/dist/components/RewindCommand.d.ts +9 -0
  65. package/dist/components/RewindCommand.d.ts.map +1 -0
  66. package/dist/components/RewindCommand.js +42 -0
  67. package/dist/components/SessionSelector.d.ts +11 -0
  68. package/dist/components/SessionSelector.d.ts.map +1 -0
  69. package/dist/components/SessionSelector.js +38 -0
  70. package/dist/components/SubagentBlock.d.ts.map +1 -1
  71. package/dist/components/SubagentBlock.js +20 -1
  72. package/dist/components/ToolResultDisplay.js +1 -1
  73. package/dist/contexts/PluginManagerContext.d.ts +4 -0
  74. package/dist/contexts/PluginManagerContext.d.ts.map +1 -0
  75. package/dist/contexts/PluginManagerContext.js +9 -0
  76. package/dist/contexts/useChat.d.ts +2 -0
  77. package/dist/contexts/useChat.d.ts.map +1 -1
  78. package/dist/contexts/useChat.js +21 -0
  79. package/dist/hooks/useInputManager.d.ts +6 -14
  80. package/dist/hooks/useInputManager.d.ts.map +1 -1
  81. package/dist/hooks/useInputManager.js +29 -45
  82. package/dist/hooks/usePluginManager.d.ts +3 -0
  83. package/dist/hooks/usePluginManager.d.ts.map +1 -0
  84. package/dist/hooks/usePluginManager.js +223 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +150 -177
  87. package/dist/managers/InputManager.d.ts +12 -21
  88. package/dist/managers/InputManager.d.ts.map +1 -1
  89. package/dist/managers/InputManager.js +77 -108
  90. package/dist/plugin-manager-cli.d.ts +6 -0
  91. package/dist/plugin-manager-cli.d.ts.map +1 -0
  92. package/dist/plugin-manager-cli.js +12 -0
  93. package/dist/session-selector-cli.d.ts +2 -0
  94. package/dist/session-selector-cli.d.ts.map +1 -0
  95. package/dist/session-selector-cli.js +25 -0
  96. package/package.json +7 -3
  97. package/src/commands/plugin/disable.ts +7 -3
  98. package/src/commands/plugin/enable.ts +7 -3
  99. package/src/commands/plugin/install.ts +2 -1
  100. package/src/commands/plugin/list.ts +21 -3
  101. package/src/commands/plugin/marketplace.ts +17 -1
  102. package/src/commands/plugin/uninstall.ts +39 -0
  103. package/src/commands/plugin/update.ts +19 -0
  104. package/src/components/ChatInterface.tsx +2 -1
  105. package/src/components/CommandSelector.tsx +7 -0
  106. package/src/components/Confirmation.tsx +1 -1
  107. package/src/components/DiscoverView.tsx +31 -0
  108. package/src/components/FileSelector.tsx +1 -1
  109. package/src/components/HistorySearch.tsx +148 -0
  110. package/src/components/InputBox.tsx +43 -28
  111. package/src/components/InstalledView.tsx +61 -0
  112. package/src/components/Markdown.tsx +37 -26
  113. package/src/components/MarketplaceAddForm.tsx +39 -0
  114. package/src/components/MarketplaceDetail.tsx +79 -0
  115. package/src/components/MarketplaceList.tsx +52 -0
  116. package/src/components/MarketplaceView.tsx +43 -0
  117. package/src/components/PluginDetail.tsx +147 -0
  118. package/src/components/PluginList.tsx +51 -0
  119. package/src/components/PluginManagerShell.tsx +189 -0
  120. package/src/components/PluginManagerTypes.ts +47 -0
  121. package/src/components/RewindCommand.tsx +114 -0
  122. package/src/components/SessionSelector.tsx +127 -0
  123. package/src/components/SubagentBlock.tsx +29 -1
  124. package/src/components/ToolResultDisplay.tsx +2 -2
  125. package/src/contexts/PluginManagerContext.ts +15 -0
  126. package/src/contexts/useChat.tsx +26 -0
  127. package/src/hooks/useInputManager.ts +29 -61
  128. package/src/hooks/usePluginManager.ts +296 -0
  129. package/src/index.ts +241 -280
  130. package/src/managers/InputManager.ts +93 -149
  131. package/src/plugin-manager-cli.tsx +13 -0
  132. package/src/session-selector-cli.tsx +37 -0
  133. package/dist/components/BashHistorySelector.d.ts +0 -11
  134. package/dist/components/BashHistorySelector.d.ts.map +0 -1
  135. package/dist/components/BashHistorySelector.js +0 -93
  136. package/dist/hooks/usePagination.d.ts +0 -20
  137. package/dist/hooks/usePagination.d.ts.map +0 -1
  138. package/dist/hooks/usePagination.js +0 -168
  139. package/src/components/BashHistorySelector.tsx +0 -181
  140. 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
8
  import { BashShellManager } from "./BashShellManager.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,11 @@ 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
+ messages,
63
62
  } = useChat();
64
63
 
65
64
  // Input manager with all input state and functionality (including images)
@@ -81,29 +80,28 @@ export const InputBox: React.FC<InputBoxProps> = ({
81
80
  handleCommandSelect,
82
81
  handleCommandInsert,
83
82
  handleCancelCommandSelect,
84
- // Bash history selector
85
- showBashHistorySelector,
86
- bashHistorySearchQuery,
87
- handleBashHistorySelect,
88
- handleCancelBashHistorySelect,
83
+ handleHistorySearchSelect,
84
+ handleCancelHistorySearch,
89
85
  // Memory type selector
90
86
  showMemoryTypeSelector,
91
87
  memoryMessage,
92
88
  handleMemoryTypeSelect,
93
89
  handleCancelMemoryTypeSelect,
90
+ // History search
91
+ showHistorySearch,
92
+ historySearchQuery,
94
93
  // Bash/MCP Manager
95
94
  showBashManager,
96
95
  showMcpManager,
96
+ showRewindManager,
97
97
  setShowBashManager,
98
98
  setShowMcpManager,
99
+ setShowRewindManager,
99
100
  // Permission mode
100
101
  permissionMode,
101
102
  setPermissionMode,
102
103
  // Input history
103
104
  setUserInputHistory,
104
- // Complex handlers combining multiple operations
105
- handleBashHistoryExecuteAndSend,
106
- handleBashHistoryDelete,
107
105
  // Main handler
108
106
  handleInput,
109
107
  // Manager ready state
@@ -138,9 +136,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
138
136
  );
139
137
  });
140
138
 
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
139
+ const handleRewindCancel = () => {
140
+ if (setShowRewindManager) {
141
+ setShowRewindManager(false);
142
+ }
143
+ };
144
144
 
145
145
  const isPlaceholder = !inputText;
146
146
  const placeholderText = INPUT_PLACEHOLDER_TEXT;
@@ -154,7 +154,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
154
154
  cursorPosition < displayText.length ? displayText[cursorPosition] : " ";
155
155
  const afterCursor = displayText.substring(cursorPosition + 1);
156
156
 
157
- // Always show cursor, allow user to continue input during loading
157
+ // Always show cursor, allow user to continue input during memory mode
158
158
  const shouldShowCursor = true;
159
159
 
160
160
  // Only show the Box after InputManager is created on first mount
@@ -162,6 +162,23 @@ export const InputBox: React.FC<InputBoxProps> = ({
162
162
  return null;
163
163
  }
164
164
 
165
+ const handleRewindSelectWithClose = async (index: number) => {
166
+ if (setShowRewindManager) {
167
+ setShowRewindManager(false);
168
+ }
169
+ await handleRewindSelect(index);
170
+ };
171
+
172
+ if (showRewindManager) {
173
+ return (
174
+ <RewindCommand
175
+ messages={messages}
176
+ onSelect={handleRewindSelectWithClose}
177
+ onCancel={handleRewindCancel}
178
+ />
179
+ );
180
+ }
181
+
165
182
  return (
166
183
  <Box flexDirection="column">
167
184
  {showFileSelector && (
@@ -183,14 +200,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
183
200
  />
184
201
  )}
185
202
 
186
- {showBashHistorySelector && (
187
- <BashHistorySelector
188
- searchQuery={bashHistorySearchQuery}
189
- workdir={currentWorkdir}
190
- onSelect={handleBashHistorySelect}
191
- onExecute={handleBashHistoryExecuteAndSend}
192
- onDelete={handleBashHistoryDelete}
193
- onCancel={handleCancelBashHistorySelect}
203
+ {showHistorySearch && (
204
+ <HistorySearch
205
+ searchQuery={historySearchQuery}
206
+ onSelect={handleHistorySearchSelect}
207
+ onCancel={handleCancelHistorySearch}
194
208
  />
195
209
  )}
196
210
 
@@ -214,7 +228,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
214
228
  onDisconnectServer={disconnectMcpServer}
215
229
  />
216
230
  )}
217
- {showBashManager || showMcpManager || (
231
+
232
+ {showBashManager || showMcpManager || showRewindManager || (
218
233
  <Box flexDirection="column">
219
234
  <Box
220
235
  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
  }
@@ -261,14 +274,12 @@ const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
261
274
  if (itemToken.type === "text") {
262
275
  const it = itemToken as Tokens.Text;
263
276
  return (
264
- <Box
265
- key={itemIndex}
266
- flexDirection="row"
267
- flexWrap="wrap"
268
- >
269
- <InlineRenderer
270
- tokens={it.tokens || [itemToken]}
271
- />
277
+ <Box key={itemIndex} flexDirection="row">
278
+ <Text>
279
+ <InlineRenderer
280
+ tokens={it.tokens || [itemToken]}
281
+ />
282
+ </Text>
272
283
  </Box>
273
284
  );
274
285
  }
@@ -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
+ };