wave-code 0.18.2 → 0.18.4

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 (45) hide show
  1. package/dist/components/CommandSelector.d.ts.map +1 -1
  2. package/dist/components/CommandSelector.js +9 -13
  3. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  4. package/dist/components/ConfirmationSelector.js +0 -1
  5. package/dist/components/DiscoverView.d.ts.map +1 -1
  6. package/dist/components/DiscoverView.js +10 -6
  7. package/dist/components/FileSelector.js +8 -8
  8. package/dist/components/HistorySearch.d.ts.map +1 -1
  9. package/dist/components/HistorySearch.js +6 -13
  10. package/dist/components/InstalledView.d.ts.map +1 -1
  11. package/dist/components/InstalledView.js +10 -6
  12. package/dist/components/LoginCommand.d.ts.map +1 -1
  13. package/dist/components/LoginCommand.js +14 -11
  14. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  15. package/dist/components/MarketplaceDetail.js +11 -7
  16. package/dist/components/MarketplaceView.d.ts.map +1 -1
  17. package/dist/components/MarketplaceView.js +10 -6
  18. package/dist/components/ModelSelector.d.ts.map +1 -1
  19. package/dist/components/ModelSelector.js +9 -15
  20. package/dist/components/SessionSelector.d.ts +5 -3
  21. package/dist/components/SessionSelector.d.ts.map +1 -1
  22. package/dist/components/SessionSelector.js +11 -7
  23. package/dist/components/WorktreeExitPrompt.d.ts.map +1 -1
  24. package/dist/components/WorktreeExitPrompt.js +3 -3
  25. package/dist/reducers/questionReducer.d.ts +0 -7
  26. package/dist/reducers/questionReducer.d.ts.map +1 -1
  27. package/dist/reducers/questionReducer.js +2 -1
  28. package/dist/reducers/selectorReducer.d.ts +7 -5
  29. package/dist/reducers/selectorReducer.d.ts.map +1 -1
  30. package/dist/reducers/selectorReducer.js +4 -2
  31. package/package.json +2 -2
  32. package/src/components/CommandSelector.tsx +11 -15
  33. package/src/components/ConfirmationSelector.tsx +0 -1
  34. package/src/components/DiscoverView.tsx +22 -7
  35. package/src/components/FileSelector.tsx +9 -9
  36. package/src/components/HistorySearch.tsx +7 -14
  37. package/src/components/InstalledView.tsx +23 -9
  38. package/src/components/LoginCommand.tsx +33 -36
  39. package/src/components/MarketplaceDetail.tsx +25 -10
  40. package/src/components/MarketplaceView.tsx +16 -7
  41. package/src/components/ModelSelector.tsx +13 -16
  42. package/src/components/SessionSelector.tsx +19 -9
  43. package/src/components/WorktreeExitPrompt.tsx +7 -4
  44. package/src/reducers/questionReducer.ts +2 -6
  45. package/src/reducers/selectorReducer.ts +20 -10
@@ -1,22 +1,24 @@
1
1
  import { Key } from "ink";
2
- export interface SelectorState {
2
+ export interface SelectorState<T = unknown> {
3
3
  selectedIndex: number;
4
4
  pendingDecision: "select" | "insert" | "cancel" | null;
5
+ items: T[];
5
6
  }
6
- export type SelectorAction = {
7
+ export type SelectorAction<T = unknown> = {
7
8
  type: "MOVE_UP";
8
9
  } | {
9
10
  type: "MOVE_DOWN";
10
- maxIndex: number;
11
11
  } | {
12
12
  type: "RESET_INDEX";
13
+ } | {
14
+ type: "SET_ITEMS";
15
+ items: T[];
13
16
  } | {
14
17
  type: "HANDLE_KEY";
15
18
  key: Key;
16
- maxIndex: number;
17
19
  hasInsert: boolean;
18
20
  } | {
19
21
  type: "CLEAR_DECISION";
20
22
  };
21
- export declare function selectorReducer(state: SelectorState, action: SelectorAction): SelectorState;
23
+ export declare function selectorReducer<T = unknown>(state: SelectorState<T>, action: SelectorAction<T>): SelectorState<T>;
22
24
  //# sourceMappingURL=selectorReducer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"selectorReducer.d.ts","sourceRoot":"","sources":["../../src/reducers/selectorReducer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;CACxD;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE/B,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,cAAc,GACrB,aAAa,CAuCf"}
1
+ {"version":3,"file":"selectorReducer.d.ts","sourceRoot":"","sources":["../../src/reducers/selectorReducer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvD,KAAK,EAAE,CAAC,EAAE,CAAC;CACZ;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,GAAG,OAAO,IAClC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,EAAE,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE/B,wBAAgB,eAAe,CAAC,CAAC,GAAG,OAAO,EACzC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,CAAC,CA+ClB"}
@@ -5,10 +5,12 @@ export function selectorReducer(state, action) {
5
5
  case "MOVE_DOWN":
6
6
  return {
7
7
  ...state,
8
- selectedIndex: Math.min(action.maxIndex, state.selectedIndex + 1),
8
+ selectedIndex: Math.min(state.items.length - 1, state.selectedIndex + 1),
9
9
  };
10
10
  case "RESET_INDEX":
11
11
  return { ...state, selectedIndex: 0 };
12
+ case "SET_ITEMS":
13
+ return { ...state, items: action.items, selectedIndex: 0 };
12
14
  case "HANDLE_KEY":
13
15
  if (action.key.upArrow) {
14
16
  return {
@@ -19,7 +21,7 @@ export function selectorReducer(state, action) {
19
21
  if (action.key.downArrow) {
20
22
  return {
21
23
  ...state,
22
- selectedIndex: Math.min(action.maxIndex, state.selectedIndex + 1),
24
+ selectedIndex: Math.min(state.items.length - 1, state.selectedIndex + 1),
23
25
  };
24
26
  }
25
27
  if (action.key.return) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,7 +42,7 @@
42
42
  "semver": "^7.7.4",
43
43
  "yargs": "^17.7.2",
44
44
  "zod": "^3.23.8",
45
- "wave-agent-sdk": "0.18.2"
45
+ "wave-agent-sdk": "0.18.4"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^19.1.8",
@@ -23,20 +23,16 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
23
23
  commands = [],
24
24
  }) => {
25
25
  const MAX_VISIBLE_ITEMS = 3;
26
- const [state, dispatch] = useReducer(selectorReducer, {
26
+ const [state, dispatch] = useReducer(selectorReducer<SlashCommand>, {
27
27
  selectedIndex: 0,
28
28
  pendingDecision: null,
29
- } as SelectorState);
29
+ items: [],
30
+ } as SelectorState<SlashCommand>);
30
31
 
31
- const { selectedIndex, pendingDecision } = state;
32
-
33
- // Reset selected index when search query changes
34
- useEffect(() => {
35
- dispatch({ type: "RESET_INDEX" });
36
- }, [searchQuery]);
32
+ const { selectedIndex, pendingDecision, items: filteredCommands } = state;
37
33
 
38
34
  // Merge agent commands and local commands, memoized to stabilize useEffect dependencies
39
- const filteredCommands = useMemo(() => {
35
+ const computedCommands = useMemo(() => {
40
36
  const allCommands = [...AVAILABLE_COMMANDS, ...commands];
41
37
  return allCommands.filter(
42
38
  (command) =>
@@ -45,6 +41,11 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
45
41
  );
46
42
  }, [searchQuery, commands]);
47
43
 
44
+ // Sync computed commands into reducer state (SET_ITEMS also resets index)
45
+ useEffect(() => {
46
+ dispatch({ type: "SET_ITEMS", items: computedCommands });
47
+ }, [computedCommands]);
48
+
48
49
  // Handle decisions from reducer
49
50
  useEffect(() => {
50
51
  if (!pendingDecision) return;
@@ -91,12 +92,7 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
91
92
  );
92
93
 
93
94
  useInput((input, key) => {
94
- dispatch({
95
- type: "HANDLE_KEY",
96
- key,
97
- maxIndex: filteredCommands.length - 1,
98
- hasInsert: !!onInsert,
99
- });
95
+ dispatch({ type: "HANDLE_KEY", key, hasInsert: !!onInsert });
100
96
  });
101
97
 
102
98
  if (filteredCommands.length === 0) {
@@ -108,7 +108,6 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
108
108
  type: "HANDLE_KEY",
109
109
  input,
110
110
  key,
111
- currentQuestion,
112
111
  questions,
113
112
  });
114
113
  } else {
@@ -1,37 +1,52 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, useInput } from "ink";
3
+ import type { MarketplacePluginEntry } from "wave-agent-sdk";
3
4
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
5
  import { PluginList } from "./PluginList.js";
5
- import { selectorReducer } from "../reducers/selectorReducer.js";
6
+ import {
7
+ selectorReducer,
8
+ type SelectorState,
9
+ } from "../reducers/selectorReducer.js";
10
+
11
+ type DiscoverablePlugin = MarketplacePluginEntry & {
12
+ marketplace: string;
13
+ installed: boolean;
14
+ version?: string;
15
+ };
6
16
 
7
17
  export const DiscoverView: React.FC = () => {
8
18
  const { discoverablePlugins, actions } = usePluginManagerContext();
9
- const [state, dispatch] = useReducer(selectorReducer, {
19
+ const [state, dispatch] = useReducer(selectorReducer<DiscoverablePlugin>, {
10
20
  selectedIndex: 0,
11
21
  pendingDecision: null,
12
- });
22
+ items: [],
23
+ } as SelectorState<DiscoverablePlugin>);
13
24
 
14
- const { selectedIndex, pendingDecision } = state;
25
+ const { selectedIndex, pendingDecision, items } = state;
26
+
27
+ // Sync plugins into reducer state
28
+ useEffect(() => {
29
+ dispatch({ type: "SET_ITEMS", items: discoverablePlugins });
30
+ }, [discoverablePlugins]);
15
31
 
16
32
  useInput((_input, key) => {
17
33
  dispatch({
18
34
  type: "HANDLE_KEY",
19
35
  key,
20
- maxIndex: discoverablePlugins.length - 1,
21
36
  hasInsert: false,
22
37
  });
23
38
  });
24
39
 
25
40
  useEffect(() => {
26
41
  if (pendingDecision === "select") {
27
- const plugin = discoverablePlugins[selectedIndex];
42
+ const plugin = items[selectedIndex];
28
43
  if (plugin) {
29
44
  actions.setSelectedId(`${plugin.name}@${plugin.marketplace}`);
30
45
  actions.setView("PLUGIN_DETAIL");
31
46
  }
32
47
  dispatch({ type: "CLEAR_DECISION" });
33
48
  }
34
- }, [pendingDecision, selectedIndex, discoverablePlugins, actions]);
49
+ }, [pendingDecision, selectedIndex, items, actions]);
35
50
 
36
51
  return (
37
52
  <Box flexDirection="column">
@@ -23,16 +23,17 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
23
23
  onSelect,
24
24
  onCancel,
25
25
  }) => {
26
- const [state, dispatch] = useReducer(selectorReducer, {
26
+ const [state, dispatch] = useReducer(selectorReducer<FileItem>, {
27
27
  selectedIndex: 0,
28
28
  pendingDecision: null,
29
- } as SelectorState);
29
+ items: [],
30
+ } as SelectorState<FileItem>);
30
31
 
31
- const { selectedIndex, pendingDecision } = state;
32
+ const { selectedIndex, pendingDecision, items: filesInState } = state;
32
33
 
33
- // Reset selected index when files change
34
+ // Sync files from props into reducer state (SET_ITEMS also resets index)
34
35
  useEffect(() => {
35
- dispatch({ type: "RESET_INDEX" });
36
+ dispatch({ type: "SET_ITEMS", items: files });
36
37
  }, [files]);
37
38
 
38
39
  // Handle decisions from reducer
@@ -40,21 +41,20 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
40
41
  if (!pendingDecision) return;
41
42
 
42
43
  if (pendingDecision === "select" || pendingDecision === "insert") {
43
- if (files.length > 0 && selectedIndex < files.length) {
44
- onSelect(files[selectedIndex].path);
44
+ if (filesInState.length > 0 && selectedIndex < filesInState.length) {
45
+ onSelect(filesInState[selectedIndex].path);
45
46
  }
46
47
  } else if (pendingDecision === "cancel") {
47
48
  onCancel();
48
49
  }
49
50
 
50
51
  dispatch({ type: "CLEAR_DECISION" });
51
- }, [pendingDecision, selectedIndex, files, onSelect, onCancel]);
52
+ }, [pendingDecision, selectedIndex, filesInState, onSelect, onCancel]);
52
53
 
53
54
  useInput((input, key) => {
54
55
  dispatch({
55
56
  type: "HANDLE_KEY",
56
57
  key,
57
- maxIndex: files.length - 1,
58
58
  hasInsert: true, // For FileSelector, Tab is also select
59
59
  });
60
60
  });
@@ -1,4 +1,4 @@
1
- import React, { useReducer, useEffect, useState } from "react";
1
+ import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { PromptHistoryManager, type PromptEntry } from "wave-agent-sdk";
4
4
  import {
@@ -18,20 +18,18 @@ export const HistorySearch: React.FC<HistorySearchProps> = ({
18
18
  onCancel,
19
19
  }) => {
20
20
  const MAX_VISIBLE_ITEMS = 5;
21
- const [state, dispatch] = useReducer(selectorReducer, {
21
+ const [state, dispatch] = useReducer(selectorReducer<PromptEntry>, {
22
22
  selectedIndex: 0,
23
23
  pendingDecision: null,
24
- } as SelectorState);
25
- const [entries, setEntries] = useState<PromptEntry[]>([]);
24
+ items: [],
25
+ } as SelectorState<PromptEntry>);
26
26
 
27
- const { selectedIndex, pendingDecision } = state;
27
+ const { selectedIndex, pendingDecision, items: entries } = state;
28
28
 
29
29
  useEffect(() => {
30
30
  const fetchHistory = async () => {
31
31
  const results = await PromptHistoryManager.searchHistory(searchQuery);
32
- const limitedResults = results.slice(0, 20);
33
- setEntries(limitedResults);
34
- dispatch({ type: "RESET_INDEX" });
32
+ dispatch({ type: "SET_ITEMS", items: results.slice(0, 20) });
35
33
  };
36
34
  fetchHistory();
37
35
  }, [searchQuery]);
@@ -52,12 +50,7 @@ export const HistorySearch: React.FC<HistorySearchProps> = ({
52
50
  }, [pendingDecision, selectedIndex, entries, onSelect, onCancel]);
53
51
 
54
52
  useInput((input, key) => {
55
- dispatch({
56
- type: "HANDLE_KEY",
57
- key,
58
- maxIndex: entries.length - 1,
59
- hasInsert: false,
60
- });
53
+ dispatch({ type: "HANDLE_KEY", key, hasInsert: false });
61
54
  });
62
55
 
63
56
  if (entries.length === 0) {
@@ -1,36 +1,50 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
+ import type { InstalledPlugin } from "wave-agent-sdk";
3
4
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
- import { selectorReducer } from "../reducers/selectorReducer.js";
5
+ import {
6
+ selectorReducer,
7
+ type SelectorState,
8
+ } from "../reducers/selectorReducer.js";
9
+
10
+ type InstalledPluginWithEnabled = InstalledPlugin & { enabled: boolean };
5
11
 
6
12
  export const InstalledView: React.FC = () => {
7
13
  const { installedPlugins, actions } = usePluginManagerContext();
8
- const [state, dispatch] = useReducer(selectorReducer, {
9
- selectedIndex: 0,
10
- pendingDecision: null,
11
- });
14
+ const [state, dispatch] = useReducer(
15
+ selectorReducer<InstalledPluginWithEnabled>,
16
+ {
17
+ selectedIndex: 0,
18
+ pendingDecision: null,
19
+ items: [],
20
+ } as SelectorState<InstalledPluginWithEnabled>,
21
+ );
12
22
 
13
- const { selectedIndex, pendingDecision } = state;
23
+ const { selectedIndex, pendingDecision, items } = state;
24
+
25
+ // Sync plugins into reducer state
26
+ useEffect(() => {
27
+ dispatch({ type: "SET_ITEMS", items: installedPlugins });
28
+ }, [installedPlugins]);
14
29
 
15
30
  useInput((_input, key) => {
16
31
  dispatch({
17
32
  type: "HANDLE_KEY",
18
33
  key,
19
- maxIndex: installedPlugins.length - 1,
20
34
  hasInsert: false,
21
35
  });
22
36
  });
23
37
 
24
38
  useEffect(() => {
25
39
  if (pendingDecision === "select") {
26
- const plugin = installedPlugins[selectedIndex];
40
+ const plugin = items[selectedIndex];
27
41
  if (plugin) {
28
42
  actions.setSelectedId(`${plugin.name}@${plugin.marketplace}`);
29
43
  actions.setView("PLUGIN_DETAIL");
30
44
  }
31
45
  dispatch({ type: "CLEAR_DECISION" });
32
46
  }
33
- }, [pendingDecision, selectedIndex, installedPlugins, actions]);
47
+ }, [pendingDecision, selectedIndex, items, actions]);
34
48
 
35
49
  if (installedPlugins.length === 0) {
36
50
  return (
@@ -36,49 +36,46 @@ export const LoginCommand: React.FC<LoginCommandProps> = ({ onCancel }) => {
36
36
  const tokenResolveRef = useRef<((token: string) => void) | null>(null);
37
37
  const tokenRejectRef = useRef<((err: Error) => void) | null>(null);
38
38
 
39
- // Pre-loading input: Enter starts login, Esc cancels
40
- useInput(
41
- (_, key) => {
39
+ // Single useInput handler branches on isLoading to avoid the
40
+ // isActive transition race that drops stdin between hook swaps.
41
+ useInput((input, key) => {
42
+ if (!isLoadingRef.current) {
43
+ // Pre-loading mode: Enter starts login, Esc cancels
42
44
  if (key.escape) {
43
45
  onCancel();
44
46
  }
45
- if (key.return && !isLoadingRef.current) {
47
+ if (key.return) {
46
48
  handleEnter();
47
49
  }
48
- },
49
- { isActive: !isLoading },
50
- );
50
+ return;
51
+ }
51
52
 
52
- // Token input: capture keystrokes while loading
53
- useInput(
54
- (input, key) => {
55
- if (key.escape) {
56
- tokenRejectRef.current?.(new Error("cancelled"));
57
- onCancel();
58
- }
59
- if (key.return) {
60
- // Submit token
61
- const trimmed = tokenInput.trim();
62
- if (trimmed) {
63
- tokenResolveRef.current?.(trimmed);
64
- } else {
65
- // Empty: just clear, keep waiting
66
- setTokenInput("");
67
- }
68
- return;
69
- }
70
- // Backspace
71
- if (key.backspace && tokenInput.length > 0) {
72
- setTokenInput((prev) => prev.slice(0, -1));
73
- return;
74
- }
75
- // Regular character input (single or pasted multi-char)
76
- if (input && !key.ctrl && !key.meta && !key.return && input.length > 0) {
77
- setTokenInput((prev) => prev + input);
53
+ // Token input mode: capture keystrokes while loading
54
+ if (key.escape) {
55
+ tokenRejectRef.current?.(new Error("cancelled"));
56
+ onCancel();
57
+ }
58
+ if (key.return) {
59
+ // Submit token
60
+ const trimmed = tokenInput.trim();
61
+ if (trimmed) {
62
+ tokenResolveRef.current?.(trimmed);
63
+ } else {
64
+ // Empty: just clear, keep waiting
65
+ setTokenInput("");
78
66
  }
79
- },
80
- { isActive: isLoading },
81
- );
67
+ return;
68
+ }
69
+ // Backspace
70
+ if (key.backspace && tokenInput.length > 0) {
71
+ setTokenInput((prev) => prev.slice(0, -1));
72
+ return;
73
+ }
74
+ // Regular character input (single or pasted multi-char)
75
+ if (input && !key.ctrl && !key.meta && !key.return && input.length > 0) {
76
+ setTokenInput((prev) => prev + input);
77
+ }
78
+ });
82
79
 
83
80
  const handleEnter = async () => {
84
81
  if (isLoadingRef.current) return;
@@ -1,16 +1,27 @@
1
1
  import React, { useReducer, useEffect, useMemo } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
- import { selectorReducer } from "../reducers/selectorReducer.js";
4
+ import {
5
+ selectorReducer,
6
+ type SelectorState,
7
+ } from "../reducers/selectorReducer.js";
5
8
 
6
9
  export const MarketplaceDetail: React.FC = () => {
7
10
  const { state, marketplaces, actions } = usePluginManagerContext();
8
- const [selectorState, dispatch] = useReducer(selectorReducer, {
9
- selectedIndex: 0,
10
- pendingDecision: null,
11
- });
11
+ const [selectorState, dispatch] = useReducer(
12
+ selectorReducer<{ id: string; label: string }>,
13
+ {
14
+ selectedIndex: 0,
15
+ pendingDecision: null,
16
+ items: [],
17
+ } as SelectorState<{ id: string; label: string }>,
18
+ );
12
19
 
13
- const { selectedIndex: selectedActionIndex, pendingDecision } = selectorState;
20
+ const {
21
+ selectedIndex: selectedActionIndex,
22
+ pendingDecision,
23
+ items: actionsList,
24
+ } = selectorState;
14
25
 
15
26
  const marketplace = marketplaces.find((m) => m.name === state.selectedId);
16
27
 
@@ -27,13 +38,17 @@ export const MarketplaceDetail: React.FC = () => {
27
38
  [marketplace?.autoUpdate],
28
39
  );
29
40
 
41
+ // Sync ACTIONS into reducer state
42
+ useEffect(() => {
43
+ dispatch({ type: "SET_ITEMS", items: [...ACTIONS] });
44
+ }, [ACTIONS]);
45
+
30
46
  useInput((_input, key) => {
31
47
  if (state.isLoading && !key.escape) return;
32
48
 
33
49
  dispatch({
34
50
  type: "HANDLE_KEY",
35
51
  key,
36
- maxIndex: ACTIONS.length - 1,
37
52
  hasInsert: false,
38
53
  });
39
54
  });
@@ -42,12 +57,12 @@ export const MarketplaceDetail: React.FC = () => {
42
57
  if (!pendingDecision) return;
43
58
 
44
59
  if (pendingDecision === "select" && marketplace && !state.isLoading) {
45
- const action = ACTIONS[selectedActionIndex].id;
60
+ const action = actionsList[selectedActionIndex]?.id;
46
61
  if (action === "toggle-auto-update") {
47
62
  actions.toggleAutoUpdate(marketplace.name, !marketplace.autoUpdate);
48
63
  } else if (action === "update") {
49
64
  actions.updateMarketplace(marketplace.name);
50
- } else {
65
+ } else if (action === "remove") {
51
66
  actions.removeMarketplace(marketplace.name);
52
67
  }
53
68
  } else if (pendingDecision === "cancel") {
@@ -58,10 +73,10 @@ export const MarketplaceDetail: React.FC = () => {
58
73
  }, [
59
74
  pendingDecision,
60
75
  selectedActionIndex,
76
+ actionsList,
61
77
  marketplace,
62
78
  state.isLoading,
63
79
  actions,
64
- ACTIONS,
65
80
  ]);
66
81
 
67
82
  if (!marketplace) {
@@ -1,18 +1,28 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
+ import type { KnownMarketplace } from "wave-agent-sdk";
3
4
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
- import { selectorReducer } from "../reducers/selectorReducer.js";
5
+ import {
6
+ selectorReducer,
7
+ type SelectorState,
8
+ } from "../reducers/selectorReducer.js";
5
9
 
6
10
  import { MarketplaceList } from "./MarketplaceList.js";
7
11
 
8
12
  export const MarketplaceView: React.FC = () => {
9
13
  const { marketplaces, actions } = usePluginManagerContext();
10
- const [state, dispatch] = useReducer(selectorReducer, {
14
+ const [state, dispatch] = useReducer(selectorReducer<KnownMarketplace>, {
11
15
  selectedIndex: 0,
12
16
  pendingDecision: null,
13
- });
17
+ items: [],
18
+ } as SelectorState<KnownMarketplace>);
19
+
20
+ const { selectedIndex, pendingDecision, items } = state;
14
21
 
15
- const { selectedIndex, pendingDecision } = state;
22
+ // Sync marketplaces into reducer state
23
+ useEffect(() => {
24
+ dispatch({ type: "SET_ITEMS", items: marketplaces });
25
+ }, [marketplaces]);
16
26
 
17
27
  useInput((input, key) => {
18
28
  if (input === "a") {
@@ -23,21 +33,20 @@ export const MarketplaceView: React.FC = () => {
23
33
  dispatch({
24
34
  type: "HANDLE_KEY",
25
35
  key,
26
- maxIndex: marketplaces.length - 1,
27
36
  hasInsert: false,
28
37
  });
29
38
  });
30
39
 
31
40
  useEffect(() => {
32
41
  if (pendingDecision === "select") {
33
- const mk = marketplaces[selectedIndex];
42
+ const mk = items[selectedIndex];
34
43
  if (mk) {
35
44
  actions.setSelectedId(mk.name);
36
45
  actions.setView("MARKETPLACE_DETAIL");
37
46
  }
38
47
  dispatch({ type: "CLEAR_DECISION" });
39
48
  }
40
- }, [pendingDecision, selectedIndex, marketplaces, actions]);
49
+ }, [pendingDecision, selectedIndex, items, actions]);
41
50
 
42
51
  return (
43
52
  <Box flexDirection="column">
@@ -1,6 +1,9 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
- import { selectorReducer } from "../reducers/selectorReducer.js";
3
+ import {
4
+ selectorReducer,
5
+ type SelectorState,
6
+ } from "../reducers/selectorReducer.js";
4
7
 
5
8
  export interface ModelSelectorProps {
6
9
  onCancel: () => void;
@@ -17,29 +20,29 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
17
20
  configuredModels,
18
21
  onSelectModel,
19
22
  }) => {
20
- const [state, dispatch] = useReducer(selectorReducer, {
23
+ const [state, dispatch] = useReducer(selectorReducer<string>, {
21
24
  selectedIndex:
22
25
  configuredModels.indexOf(currentModel) !== -1
23
26
  ? configuredModels.indexOf(currentModel)
24
27
  : 0,
25
28
  pendingDecision: null,
26
- });
29
+ items: configuredModels,
30
+ } as SelectorState<string>);
27
31
 
28
- const { selectedIndex, pendingDecision } = state;
32
+ const { selectedIndex, pendingDecision, items: models } = state;
29
33
 
30
34
  useInput((_input, key) => {
31
35
  dispatch({
32
36
  type: "HANDLE_KEY",
33
37
  key,
34
- maxIndex: configuredModels.length - 1,
35
38
  hasInsert: false,
36
39
  });
37
40
  });
38
41
 
39
42
  useEffect(() => {
40
43
  if (pendingDecision === "select") {
41
- if (configuredModels.length > 0) {
42
- onSelectModel(configuredModels[selectedIndex]);
44
+ if (models.length > 0) {
45
+ onSelectModel(models[selectedIndex]);
43
46
  }
44
47
  onCancel();
45
48
  dispatch({ type: "CLEAR_DECISION" });
@@ -47,23 +50,17 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
47
50
  onCancel();
48
51
  dispatch({ type: "CLEAR_DECISION" });
49
52
  }
50
- }, [
51
- pendingDecision,
52
- selectedIndex,
53
- configuredModels,
54
- onSelectModel,
55
- onCancel,
56
- ]);
53
+ }, [pendingDecision, selectedIndex, models, onSelectModel, onCancel]);
57
54
 
58
55
  // Calculate visible window
59
56
  const startIndex = Math.max(
60
57
  0,
61
58
  Math.min(
62
59
  selectedIndex - Math.floor(MAX_VISIBLE_ITEMS / 2),
63
- Math.max(0, configuredModels.length - MAX_VISIBLE_ITEMS),
60
+ Math.max(0, models.length - MAX_VISIBLE_ITEMS),
64
61
  ),
65
62
  );
66
- const visibleModels = configuredModels.slice(
63
+ const visibleModels = models.slice(
67
64
  startIndex,
68
65
  startIndex + MAX_VISIBLE_ITEMS,
69
66
  );