wave-code 0.13.6 → 0.14.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 (80) hide show
  1. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  2. package/dist/components/BackgroundTaskManager.js +36 -25
  3. package/dist/components/ChatInterface.d.ts.map +1 -1
  4. package/dist/components/ChatInterface.js +1 -1
  5. package/dist/components/ConfirmationSelector.d.ts +0 -1
  6. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  7. package/dist/components/ConfirmationSelector.js +111 -259
  8. package/dist/components/InputBox.d.ts.map +1 -1
  9. package/dist/components/InputBox.js +2 -2
  10. package/dist/components/MarketplaceAddForm.d.ts.map +1 -1
  11. package/dist/components/MarketplaceAddForm.js +48 -17
  12. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  13. package/dist/components/MarketplaceDetail.js +2 -1
  14. package/dist/components/MarketplaceList.d.ts.map +1 -1
  15. package/dist/components/MarketplaceList.js +2 -1
  16. package/dist/components/McpManager.d.ts.map +1 -1
  17. package/dist/components/McpManager.js +22 -27
  18. package/dist/components/MessageList.d.ts.map +1 -1
  19. package/dist/components/MessageList.js +4 -6
  20. package/dist/components/PluginDetail.d.ts.map +1 -1
  21. package/dist/components/PluginDetail.js +22 -22
  22. package/dist/components/PluginManagerShell.d.ts +1 -0
  23. package/dist/components/PluginManagerShell.d.ts.map +1 -1
  24. package/dist/components/PluginManagerShell.js +2 -2
  25. package/dist/components/PluginManagerTypes.d.ts +2 -2
  26. package/dist/components/PluginManagerTypes.d.ts.map +1 -1
  27. package/dist/components/TaskNotificationMessage.js +1 -1
  28. package/dist/contexts/useChat.d.ts +1 -0
  29. package/dist/contexts/useChat.d.ts.map +1 -1
  30. package/dist/contexts/useChat.js +128 -102
  31. package/dist/hooks/usePluginManager.d.ts +3 -1
  32. package/dist/hooks/usePluginManager.d.ts.map +1 -1
  33. package/dist/hooks/usePluginManager.js +16 -7
  34. package/dist/managers/inputHandlers.d.ts.map +1 -1
  35. package/dist/managers/inputHandlers.js +6 -9
  36. package/dist/managers/inputReducer.d.ts +6 -0
  37. package/dist/managers/inputReducer.d.ts.map +1 -1
  38. package/dist/managers/inputReducer.js +15 -0
  39. package/dist/reducers/backgroundTaskManagerReducer.d.ts +43 -0
  40. package/dist/reducers/backgroundTaskManagerReducer.d.ts.map +1 -0
  41. package/dist/reducers/backgroundTaskManagerReducer.js +23 -0
  42. package/dist/reducers/confirmationReducer.d.ts +26 -0
  43. package/dist/reducers/confirmationReducer.d.ts.map +1 -0
  44. package/dist/reducers/confirmationReducer.js +45 -0
  45. package/dist/reducers/marketplaceAddFormReducer.d.ts +24 -0
  46. package/dist/reducers/marketplaceAddFormReducer.d.ts.map +1 -0
  47. package/dist/reducers/marketplaceAddFormReducer.js +18 -0
  48. package/dist/reducers/mcpManagerReducer.d.ts +16 -0
  49. package/dist/reducers/mcpManagerReducer.d.ts.map +1 -0
  50. package/dist/reducers/mcpManagerReducer.js +15 -0
  51. package/dist/reducers/pluginDetailReducer.d.ts +25 -0
  52. package/dist/reducers/pluginDetailReducer.d.ts.map +1 -0
  53. package/dist/reducers/pluginDetailReducer.js +38 -0
  54. package/dist/reducers/questionReducer.d.ts +69 -0
  55. package/dist/reducers/questionReducer.d.ts.map +1 -0
  56. package/dist/reducers/questionReducer.js +195 -0
  57. package/package.json +2 -2
  58. package/src/components/BackgroundTaskManager.tsx +32 -34
  59. package/src/components/ChatInterface.tsx +0 -1
  60. package/src/components/ConfirmationSelector.tsx +122 -304
  61. package/src/components/InputBox.tsx +7 -1
  62. package/src/components/MarketplaceAddForm.tsx +76 -22
  63. package/src/components/MarketplaceDetail.tsx +4 -0
  64. package/src/components/MarketplaceList.tsx +4 -0
  65. package/src/components/McpManager.tsx +30 -34
  66. package/src/components/MessageList.tsx +10 -14
  67. package/src/components/PluginDetail.tsx +25 -33
  68. package/src/components/PluginManagerShell.tsx +3 -2
  69. package/src/components/PluginManagerTypes.ts +8 -2
  70. package/src/components/TaskNotificationMessage.tsx +1 -1
  71. package/src/contexts/useChat.tsx +57 -24
  72. package/src/hooks/usePluginManager.ts +18 -7
  73. package/src/managers/inputHandlers.ts +6 -8
  74. package/src/managers/inputReducer.ts +19 -0
  75. package/src/reducers/backgroundTaskManagerReducer.ts +60 -0
  76. package/src/reducers/confirmationReducer.ts +74 -0
  77. package/src/reducers/marketplaceAddFormReducer.ts +35 -0
  78. package/src/reducers/mcpManagerReducer.ts +31 -0
  79. package/src/reducers/pluginDetailReducer.ts +58 -0
  80. package/src/reducers/questionReducer.ts +251 -0
@@ -1,53 +1,107 @@
1
- import React, { useState, useRef, useEffect } from "react";
1
+ import React, { useReducer } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
+ import { marketplaceAddFormReducer } from "../reducers/marketplaceAddFormReducer.js";
5
+
6
+ const SCOPES = [
7
+ { value: "user" as const, label: "user" },
8
+ { value: "project" as const, label: "project" },
9
+ { value: "local" as const, label: "local" },
10
+ ];
4
11
 
5
12
  export const MarketplaceAddForm: React.FC = () => {
6
13
  const { state, actions } = usePluginManagerContext();
7
- const [source, setSource] = useState("");
8
- const sourceRef = useRef(source);
9
-
10
- // Keep ref in sync with state
11
- useEffect(() => {
12
- sourceRef.current = source;
13
- }, [source]);
14
+ const [{ source, scopeIndex, step }, dispatch] = useReducer(
15
+ marketplaceAddFormReducer,
16
+ { source: "", scopeIndex: 0, step: "source" },
17
+ );
14
18
 
15
19
  useInput((input, key) => {
16
20
  if (key.escape) {
17
- actions.setView("MARKETPLACES");
21
+ if (step === "scope") {
22
+ dispatch({ type: "BACK_TO_SOURCE" });
23
+ } else {
24
+ actions.setView("MARKETPLACES");
25
+ }
18
26
  } else if (state.isLoading) {
19
27
  return;
20
- } else if (key.return) {
21
- if (sourceRef.current.trim()) {
22
- actions.addMarketplace(sourceRef.current.trim());
28
+ } else if (step === "source" && key.return) {
29
+ if (source.trim()) {
30
+ dispatch({ type: "SET_STEP", step: "scope" });
31
+ }
32
+ } else if (step === "source" && (key.backspace || key.delete)) {
33
+ dispatch({ type: "DELETE_CHAR" });
34
+ } else if (
35
+ step === "source" &&
36
+ input &&
37
+ !key.ctrl &&
38
+ !key.meta &&
39
+ !("alt" in key && key.alt)
40
+ ) {
41
+ dispatch({ type: "INSERT_CHAR", text: input });
42
+ } else if (step === "scope") {
43
+ if (key.upArrow) {
44
+ dispatch({
45
+ type: "SET_SCOPE_INDEX",
46
+ index: Math.max(0, scopeIndex - 1),
47
+ });
48
+ } else if (key.downArrow) {
49
+ dispatch({
50
+ type: "SET_SCOPE_INDEX",
51
+ index: Math.min(SCOPES.length - 1, scopeIndex + 1),
52
+ });
53
+ } else if (key.return) {
54
+ const scope = SCOPES[scopeIndex].value;
55
+ actions.addMarketplace(source.trim(), scope);
23
56
  }
24
- } else if (key.backspace || key.delete) {
25
- setSource((prev) => prev.slice(0, -1));
26
- } else if (input.length === 1) {
27
- setSource((prev) => prev + input);
28
57
  }
29
58
  });
30
59
 
60
+ if (step === "source") {
61
+ return (
62
+ <Box flexDirection="column" padding={1}>
63
+ <Text bold color="cyan">
64
+ Step 1/2: Enter marketplace source
65
+ </Text>
66
+ <Box marginTop={1}>
67
+ <Text>Source: </Text>
68
+ <Text color="yellow">{source}</Text>
69
+ {!state.isLoading && <Text color="yellow">_</Text>}
70
+ </Box>
71
+ <Box marginTop={1}>
72
+ <Text dimColor>Enter to continue, Esc to cancel</Text>
73
+ </Box>
74
+ </Box>
75
+ );
76
+ }
77
+
31
78
  return (
32
79
  <Box flexDirection="column" padding={1}>
33
80
  <Text bold color="cyan">
34
- Add Marketplace
81
+ Step 2/2: Select scope
35
82
  </Text>
36
83
  <Box marginTop={1}>
37
- <Text>Source (URL or Path): </Text>
38
- <Text color={state.isLoading ? "gray" : "yellow"}>{source}</Text>
39
- {!state.isLoading && <Text color="yellow">_</Text>}
84
+ <Text dimColor>Source: </Text>
85
+ <Text dimColor>{source}</Text>
86
+ </Box>
87
+ <Box marginTop={1} flexDirection="column">
88
+ {SCOPES.map((s, i) => (
89
+ <Text key={s.value} color={i === scopeIndex ? "yellow" : "dim"}>
90
+ {i === scopeIndex ? "> " : " "}
91
+ {s.label}
92
+ </Text>
93
+ ))}
40
94
  </Box>
41
95
  {state.isLoading && (
42
96
  <Box marginTop={1}>
43
- <Text color="yellow">⌛ Adding marketplace...</Text>
97
+ <Text color="yellow">Adding marketplace...</Text>
44
98
  </Box>
45
99
  )}
46
100
  <Box marginTop={1}>
47
101
  <Text dimColor>
48
102
  {state.isLoading
49
103
  ? "Please wait..."
50
- : "Press Enter to add, Esc to cancel"}
104
+ : "Enter to confirm, \u2191/\u2193 to navigate, Esc to go back"}
51
105
  </Text>
52
106
  </Box>
53
107
  </Box>
@@ -55,6 +55,10 @@ export const MarketplaceDetail: React.FC = () => {
55
55
  {marketplace.name}
56
56
  </Text>
57
57
  {marketplace.isBuiltin && <Text dimColor> (Built-in)</Text>}
58
+ {marketplace.declaredScope &&
59
+ marketplace.declaredScope !== "builtin" && (
60
+ <Text dimColor> ({marketplace.declaredScope} scope)</Text>
61
+ )}
58
62
  </Box>
59
63
 
60
64
  <Box marginBottom={1}>
@@ -39,6 +39,10 @@ export const MarketplaceList: React.FC<MarketplaceListProps> = ({
39
39
  {marketplace.isBuiltin && (
40
40
  <Text color="yellow"> [Built-in]</Text>
41
41
  )}
42
+ {marketplace.declaredScope &&
43
+ marketplace.declaredScope !== "builtin" && (
44
+ <Text color="magenta"> [{marketplace.declaredScope}]</Text>
45
+ )}
42
46
  </Text>
43
47
  </Box>
44
48
  <Box marginLeft={4} flexDirection="column">
@@ -1,6 +1,10 @@
1
- import React, { useState, useRef, useEffect } from "react";
1
+ import React, { useReducer } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { McpServerStatus } from "wave-agent-sdk";
4
+ import {
5
+ mcpManagerReducer,
6
+ type McpManagerState,
7
+ } from "../reducers/mcpManagerReducer.js";
4
8
 
5
9
  export interface McpManagerProps {
6
10
  onCancel: () => void;
@@ -9,33 +13,25 @@ export interface McpManagerProps {
9
13
  onDisconnectServer: (serverName: string) => Promise<boolean>;
10
14
  }
11
15
 
16
+ const initialState: McpManagerState = {
17
+ selectedIndex: 0,
18
+ viewMode: "list",
19
+ };
20
+
12
21
  export const McpManager: React.FC<McpManagerProps> = ({
13
22
  onCancel,
14
23
  servers,
15
24
  onConnectServer,
16
25
  onDisconnectServer,
17
26
  }) => {
18
- const [selectedIndex, setSelectedIndex] = useState(0);
19
- const [viewMode, setViewMode] = useState<"list" | "detail">("list");
20
-
21
- // Keep ref in sync with state to avoid stale closures in useInput
22
- const selectedIndexRef = useRef(selectedIndex);
23
- const viewModeRef = useRef(viewMode);
24
-
25
- useEffect(() => {
26
- selectedIndexRef.current = selectedIndex;
27
- }, [selectedIndex]);
28
-
29
- useEffect(() => {
30
- viewModeRef.current = viewMode;
31
- }, [viewMode]);
27
+ const [state, dispatch] = useReducer(mcpManagerReducer, initialState);
32
28
 
33
29
  // Dynamically calculate selectedServer based on selectedIndex and servers
34
30
  const selectedServer =
35
- viewMode === "detail" &&
31
+ state.viewMode === "detail" &&
36
32
  servers.length > 0 &&
37
- selectedIndex < servers.length
38
- ? servers[selectedIndex]
33
+ state.selectedIndex < servers.length
34
+ ? servers[state.selectedIndex]
39
35
  : null;
40
36
 
41
37
  const formatTime = (timestamp: number): string => {
@@ -78,15 +74,15 @@ export const McpManager: React.FC<McpManagerProps> = ({
78
74
 
79
75
  useInput((input, key) => {
80
76
  if (key.return) {
81
- if (viewModeRef.current === "list") {
82
- setViewMode("detail");
77
+ if (state.viewMode === "list") {
78
+ dispatch({ type: "SET_VIEW_MODE", viewMode: "detail" });
83
79
  }
84
80
  return;
85
81
  }
86
82
 
87
83
  if (key.escape) {
88
- if (viewModeRef.current === "detail") {
89
- setViewMode("list");
84
+ if (state.viewMode === "detail") {
85
+ dispatch({ type: "SET_VIEW_MODE", viewMode: "list" });
90
86
  } else {
91
87
  onCancel();
92
88
  }
@@ -94,18 +90,18 @@ export const McpManager: React.FC<McpManagerProps> = ({
94
90
  }
95
91
 
96
92
  if (key.upArrow) {
97
- setSelectedIndex((prev) => Math.max(0, prev - 1));
93
+ dispatch({ type: "MOVE_UP", serverCount: servers.length });
98
94
  return;
99
95
  }
100
96
 
101
97
  if (key.downArrow) {
102
- setSelectedIndex((prev) => Math.min(servers.length - 1, prev + 1));
98
+ dispatch({ type: "MOVE_DOWN", serverCount: servers.length });
103
99
  return;
104
100
  }
105
101
 
106
102
  // Hotkeys for server actions
107
103
  if (input === "c") {
108
- const server = servers[selectedIndexRef.current];
104
+ const server = servers[state.selectedIndex];
109
105
  if (
110
106
  server &&
111
107
  (server.status === "disconnected" || server.status === "error")
@@ -116,7 +112,7 @@ export const McpManager: React.FC<McpManagerProps> = ({
116
112
  }
117
113
 
118
114
  if (input === "d") {
119
- const server = servers[selectedIndexRef.current];
115
+ const server = servers[state.selectedIndex];
120
116
  if (server && server.status === "connected") {
121
117
  handleDisconnect(server.name);
122
118
  }
@@ -124,7 +120,7 @@ export const McpManager: React.FC<McpManagerProps> = ({
124
120
  }
125
121
  });
126
122
 
127
- if (viewMode === "detail" && selectedServer) {
123
+ if (state.viewMode === "detail" && selectedServer) {
128
124
  return (
129
125
  <Box
130
126
  flexDirection="column"
@@ -273,10 +269,10 @@ export const McpManager: React.FC<McpManagerProps> = ({
273
269
  {servers.map((server, index) => (
274
270
  <Box key={server.name} flexDirection="column">
275
271
  <Text
276
- color={index === selectedIndex ? "black" : "white"}
277
- backgroundColor={index === selectedIndex ? "cyan" : undefined}
272
+ color={index === state.selectedIndex ? "black" : "white"}
273
+ backgroundColor={index === state.selectedIndex ? "cyan" : undefined}
278
274
  >
279
- {index === selectedIndex ? "▶ " : " "}
275
+ {index === state.selectedIndex ? "▶ " : " "}
280
276
  {index + 1}.{" "}
281
277
  <Text color={getStatusColor(server.status)}>
282
278
  {getStatusIcon(server.status)}
@@ -286,7 +282,7 @@ export const McpManager: React.FC<McpManagerProps> = ({
286
282
  <Text color="green"> · {server.toolCount} tools</Text>
287
283
  )}
288
284
  </Text>
289
- {index === selectedIndex && (
285
+ {index === state.selectedIndex && (
290
286
  <Box marginLeft={4} flexDirection="column">
291
287
  <Text color="gray" dimColor>
292
288
  {server.config.command}
@@ -305,11 +301,11 @@ export const McpManager: React.FC<McpManagerProps> = ({
305
301
  <Box marginTop={1}>
306
302
  <Text dimColor>
307
303
  ↑/↓ to select · Enter to view ·{" "}
308
- {servers[selectedIndex]?.status === "disconnected" ||
309
- servers[selectedIndex]?.status === "error"
304
+ {servers[state.selectedIndex]?.status === "disconnected" ||
305
+ servers[state.selectedIndex]?.status === "error"
310
306
  ? "c to connect · "
311
307
  : ""}
312
- {servers[selectedIndex]?.status === "connected"
308
+ {servers[state.selectedIndex]?.status === "connected"
313
309
  ? "d to disconnect · "
314
310
  : ""}
315
311
  Esc to close
@@ -43,23 +43,19 @@ export const MessageList = React.memo(
43
43
  b.stage === "streaming" ||
44
44
  b.stage === "start")) ||
45
45
  (b.type === "bang" && b.stage === "running") ||
46
- (b.type === "reasoning" && b.stage === "streaming");
46
+ (b.type === "reasoning" && b.stage === "streaming") ||
47
+ (b.type === "text" && b.stage === "streaming");
47
48
 
48
49
  // Flatten messages into blocks with metadata
49
- // Filter out streaming text blocks to avoid frequent height changes,
50
- // but allow streaming reasoning blocks (rendered as truncated gray text)
50
+ // Include streaming text blocks (rendered as truncated gray text)
51
51
  const allBlocks = visibleMessages.flatMap((message, messageIndex) => {
52
- return message.blocks
53
- .filter(
54
- (block) => !(block.type === "text" && block.stage === "streaming"),
55
- )
56
- .map((block, blockIndex) => ({
57
- block,
58
- message,
59
- messageIndex,
60
- // Unique key for each block to help Static component
61
- key: `${message.id}-${blockIndex}`,
62
- }));
52
+ return message.blocks.map((block, blockIndex) => ({
53
+ block,
54
+ message,
55
+ messageIndex,
56
+ // Unique key for each block to help Static component
57
+ key: `${message.id}-${blockIndex}`,
58
+ }));
63
59
  });
64
60
 
65
61
  // Find message indices that have any running/streaming block
@@ -1,6 +1,10 @@
1
- import React, { useState, useRef, useEffect } from "react";
1
+ import React, { useReducer } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
+ import {
5
+ pluginDetailReducer,
6
+ type PluginDetailState,
7
+ } from "../reducers/pluginDetailReducer.js";
4
8
 
5
9
  const SCOPES = [
6
10
  { id: "project", label: "Install for all collaborators (project scope)" },
@@ -11,20 +15,10 @@ const SCOPES = [
11
15
  export const PluginDetail: React.FC = () => {
12
16
  const { state, discoverablePlugins, installedPlugins, actions } =
13
17
  usePluginManagerContext();
14
- const [selectedScopeIndex, setSelectedScopeIndex] = useState(0);
15
- const [selectedActionIndex, setSelectedActionIndex] = useState(0);
16
-
17
- // Keep refs in sync with state to avoid stale closures in useInput
18
- const selectedScopeIndexRef = useRef(selectedScopeIndex);
19
- const selectedActionIndexRef = useRef(selectedActionIndex);
20
-
21
- useEffect(() => {
22
- selectedScopeIndexRef.current = selectedScopeIndex;
23
- }, [selectedScopeIndex]);
24
-
25
- useEffect(() => {
26
- selectedActionIndexRef.current = selectedActionIndex;
27
- }, [selectedActionIndex]);
18
+ const [detailState, dispatch] = useReducer(pluginDetailReducer, {
19
+ selectedScopeIndex: 0,
20
+ selectedActionIndex: 0,
21
+ } as PluginDetailState);
28
22
 
29
23
  const plugin =
30
24
  discoverablePlugins.find(
@@ -48,22 +42,20 @@ export const PluginDetail: React.FC = () => {
48
42
  );
49
43
  actions.setView(isFromDiscover ? "DISCOVER" : "INSTALLED");
50
44
  } else if (key.upArrow) {
51
- setSelectedActionIndex((prev) =>
52
- prev > 0 ? prev - 1 : INSTALLED_ACTIONS.length - 1,
53
- );
54
- setSelectedScopeIndex((prev) =>
55
- prev > 0 ? prev - 1 : SCOPES.length - 1,
56
- );
45
+ dispatch({
46
+ type: "MOVE_ACTION_UP",
47
+ maxIndex: INSTALLED_ACTIONS.length - 1,
48
+ });
49
+ dispatch({ type: "MOVE_SCOPE_UP", maxIndex: SCOPES.length - 1 });
57
50
  } else if (key.downArrow) {
58
- setSelectedActionIndex((prev) =>
59
- prev < INSTALLED_ACTIONS.length - 1 ? prev + 1 : 0,
60
- );
61
- setSelectedScopeIndex((prev) =>
62
- prev < SCOPES.length - 1 ? prev + 1 : 0,
63
- );
51
+ dispatch({
52
+ type: "MOVE_ACTION_DOWN",
53
+ maxIndex: INSTALLED_ACTIONS.length - 1,
54
+ });
55
+ dispatch({ type: "MOVE_SCOPE_DOWN", maxIndex: SCOPES.length - 1 });
64
56
  } else if (key.return && plugin && !state.isLoading) {
65
57
  if (isInstalledAndEnabled) {
66
- const action = INSTALLED_ACTIONS[selectedActionIndexRef.current].id;
58
+ const action = INSTALLED_ACTIONS[detailState.selectedActionIndex].id;
67
59
  if (action === "uninstall") {
68
60
  actions.uninstallPlugin(plugin.name, plugin.marketplace);
69
61
  } else {
@@ -73,7 +65,7 @@ export const PluginDetail: React.FC = () => {
73
65
  actions.installPlugin(
74
66
  plugin.name,
75
67
  plugin.marketplace,
76
- SCOPES[selectedScopeIndexRef.current].id,
68
+ SCOPES[detailState.selectedScopeIndex].id,
77
69
  );
78
70
  }
79
71
  }
@@ -121,14 +113,14 @@ export const PluginDetail: React.FC = () => {
121
113
  <Text
122
114
  key={action.id}
123
115
  color={
124
- index === selectedActionIndex
116
+ index === detailState.selectedActionIndex
125
117
  ? state.isLoading
126
118
  ? "gray"
127
119
  : "yellow"
128
120
  : undefined
129
121
  }
130
122
  >
131
- {index === selectedActionIndex ? "> " : " "}
123
+ {index === detailState.selectedActionIndex ? "> " : " "}
132
124
  {action.label}
133
125
  </Text>
134
126
  ))}
@@ -147,14 +139,14 @@ export const PluginDetail: React.FC = () => {
147
139
  <Text
148
140
  key={scope.id}
149
141
  color={
150
- index === selectedScopeIndex
142
+ index === detailState.selectedScopeIndex
151
143
  ? state.isLoading
152
144
  ? "gray"
153
145
  : "green"
154
146
  : undefined
155
147
  }
156
148
  >
157
- {index === selectedScopeIndex ? "> " : " "}
149
+ {index === detailState.selectedScopeIndex ? "> " : " "}
158
150
  {scope.label}
159
151
  </Text>
160
152
  ))}
@@ -13,8 +13,9 @@ import { PluginManagerContext } from "../contexts/PluginManagerContext.js";
13
13
  export const PluginManagerShell: React.FC<{
14
14
  children?: React.ReactNode;
15
15
  onCancel?: () => void;
16
- }> = ({ children, onCancel }) => {
17
- const pluginManager = usePluginManager();
16
+ onPluginInstalled?: () => void;
17
+ }> = ({ children, onCancel, onPluginInstalled }) => {
18
+ const pluginManager = usePluginManager({ onPluginInstalled });
18
19
  const { state, actions, discoverablePlugins } = pluginManager;
19
20
 
20
21
  const setView = (view: ViewType) => {
@@ -33,8 +33,14 @@ export interface PluginManagerContextType {
33
33
  actions: {
34
34
  setView: (view: ViewType) => void;
35
35
  setSelectedId: (id: string | null) => void;
36
- addMarketplace: (source: string) => Promise<void>;
37
- removeMarketplace: (name: string) => Promise<void>;
36
+ addMarketplace: (
37
+ source: string,
38
+ scope?: "user" | "project" | "local",
39
+ ) => Promise<void>;
40
+ removeMarketplace: (
41
+ name: string,
42
+ scope?: "user" | "project" | "local",
43
+ ) => Promise<void>;
38
44
  updateMarketplace: (name: string) => Promise<void>;
39
45
  installPlugin: (
40
46
  name: string,
@@ -20,7 +20,7 @@ export const TaskNotificationMessage = ({
20
20
  return (
21
21
  <Box flexDirection="column">
22
22
  <Box>
23
- <Text color={color}>● </Text>
23
+ <Text color={color}>◆ </Text>
24
24
  <Text color="white">{block.summary}</Text>
25
25
  </Box>
26
26
  </Box>
@@ -117,6 +117,8 @@ export interface ChatContextType {
117
117
  workingDirectory: string;
118
118
  version?: string;
119
119
  workdir?: string;
120
+ // Agent recreation (e.g. after plugin install)
121
+ recreateAgent: () => void;
120
122
  }
121
123
 
122
124
  const ChatContext = createContext<ChatContextType | null>(null);
@@ -332,8 +334,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
332
334
  );
333
335
 
334
336
  // Initialize AI manager
335
- useEffect(() => {
336
- const initializeAgent = async () => {
337
+ const initializeAgent = useCallback(
338
+ async (restoreSessionIdOverride?: string) => {
339
+ const effectiveRestoreSessionId =
340
+ restoreSessionIdOverride ?? restoreSessionId;
341
+
337
342
  const callbacks: AgentCallbacks = {
338
343
  onMessagesChange: () => {
339
344
  throttledSetMessages();
@@ -400,7 +405,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
400
405
 
401
406
  const agent = await Agent.create({
402
407
  callbacks,
403
- restoreSessionId,
408
+ restoreSessionId: effectiveRestoreSessionId,
404
409
  continueLastSession,
405
410
  logger,
406
411
  permissionMode:
@@ -442,25 +447,53 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
442
447
  } catch (error) {
443
448
  console.error("Failed to initialize AI manager:", error);
444
449
  }
445
- };
450
+ },
451
+ [
452
+ restoreSessionId,
453
+ continueLastSession,
454
+ bypassPermissions,
455
+ showConfirmation,
456
+ pluginDirs,
457
+ tools,
458
+ allowedTools,
459
+ disallowedTools,
460
+ workdir,
461
+ worktreeSession,
462
+ model,
463
+ initialPermissionMode,
464
+ throttledSetMessages,
465
+ throttledSetTokens,
466
+ ],
467
+ );
446
468
 
469
+ // Recreate agent (e.g. after plugin install) — destroys current agent and reinitializes
470
+ const recreateAgent = useCallback(() => {
471
+ const currentSessionId = agentRef.current?.sessionId;
472
+ if (agentRef.current) {
473
+ try {
474
+ agentRef.current.destroy();
475
+ } catch {
476
+ // Ignore destroy errors
477
+ }
478
+ }
479
+ agentRef.current = null;
480
+ setMessages([]);
481
+ setMcpServers([]);
482
+ setSlashCommands([]);
483
+ setSessionId("");
484
+ setIsLoading(false);
485
+ setlatestTotalTokens(0);
486
+ setIsCommandRunning(false);
487
+ setIsCompressing(false);
488
+ if (currentSessionId) {
489
+ initializeAgent(currentSessionId);
490
+ }
491
+ }, [initializeAgent]);
492
+
493
+ // Run initial agent creation
494
+ useEffect(() => {
447
495
  initializeAgent();
448
- }, [
449
- restoreSessionId,
450
- continueLastSession,
451
- bypassPermissions,
452
- showConfirmation,
453
- pluginDirs,
454
- tools,
455
- allowedTools,
456
- disallowedTools,
457
- workdir,
458
- worktreeSession,
459
- model,
460
- initialPermissionMode,
461
- throttledSetMessages,
462
- throttledSetTokens,
463
- ]);
496
+ }, [initializeAgent]);
464
497
 
465
498
  // Cleanup on unmount
466
499
  useEffect(() => {
@@ -613,6 +646,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
613
646
  if (currentConfirmation) {
614
647
  currentConfirmation.reject();
615
648
  }
649
+ agentRef.current?.abortMessage();
616
650
  hideConfirmation();
617
651
  }, [currentConfirmation, hideConfirmation]);
618
652
 
@@ -697,10 +731,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
697
731
  }
698
732
 
699
733
  // Handle ESC key to cancel confirmation
700
- if (key.escape) {
701
- if (isConfirmationVisible) {
702
- handleConfirmationCancel();
703
- }
734
+ if (key.escape && isConfirmationVisible) {
735
+ handleConfirmationCancel();
704
736
  }
705
737
  });
706
738
 
@@ -752,6 +784,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
752
784
  workingDirectory,
753
785
  version,
754
786
  workdir,
787
+ recreateAgent,
755
788
  };
756
789
 
757
790
  return (