wave-code 0.14.4 → 0.15.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 (116) hide show
  1. package/dist/acp/agent.d.ts.map +1 -1
  2. package/dist/acp/agent.js +10 -0
  3. package/dist/components/BackgroundTaskManager.d.ts.map +1 -1
  4. package/dist/components/BackgroundTaskManager.js +17 -53
  5. package/dist/components/CommandSelector.d.ts.map +1 -1
  6. package/dist/components/CommandSelector.js +38 -28
  7. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  8. package/dist/components/ConfirmationSelector.js +18 -196
  9. package/dist/components/DiscoverView.d.ts.map +1 -1
  10. package/dist/components/DiscoverView.js +19 -11
  11. package/dist/components/FileSelector.d.ts.map +1 -1
  12. package/dist/components/FileSelector.js +29 -25
  13. package/dist/components/HelpView.d.ts.map +1 -1
  14. package/dist/components/HelpView.js +24 -28
  15. package/dist/components/HistorySearch.d.ts.map +1 -1
  16. package/dist/components/HistorySearch.js +26 -28
  17. package/dist/components/InputBox.d.ts.map +1 -1
  18. package/dist/components/InputBox.js +14 -6
  19. package/dist/components/InstalledView.d.ts.map +1 -1
  20. package/dist/components/InstalledView.js +19 -11
  21. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  22. package/dist/components/MarketplaceDetail.js +35 -16
  23. package/dist/components/MarketplaceView.d.ts.map +1 -1
  24. package/dist/components/MarketplaceView.js +21 -12
  25. package/dist/components/McpManager.d.ts.map +1 -1
  26. package/dist/components/McpManager.js +27 -46
  27. package/dist/components/ModelSelector.d.ts.map +1 -1
  28. package/dist/components/ModelSelector.js +27 -17
  29. package/dist/components/PluginDetail.d.ts.map +1 -1
  30. package/dist/components/PluginDetail.js +39 -26
  31. package/dist/components/RewindCommand.d.ts.map +1 -1
  32. package/dist/components/RewindCommand.js +27 -21
  33. package/dist/components/SessionSelector.d.ts.map +1 -1
  34. package/dist/components/SessionSelector.js +21 -16
  35. package/dist/components/StatusLine.d.ts.map +1 -1
  36. package/dist/components/StatusLine.js +5 -1
  37. package/dist/components/WorktreeExitPrompt.d.ts.map +1 -1
  38. package/dist/components/WorktreeExitPrompt.js +21 -12
  39. package/dist/contexts/useChat.d.ts +0 -1
  40. package/dist/contexts/useChat.d.ts.map +1 -1
  41. package/dist/contexts/useChat.js +13 -20
  42. package/dist/hooks/useInputManager.d.ts +5 -19
  43. package/dist/hooks/useInputManager.d.ts.map +1 -1
  44. package/dist/hooks/useInputManager.js +222 -79
  45. package/dist/managers/inputHandlers.d.ts +1 -1
  46. package/dist/managers/inputHandlers.d.ts.map +1 -1
  47. package/dist/managers/inputHandlers.js +3 -6
  48. package/dist/managers/inputReducer.d.ts +46 -4
  49. package/dist/managers/inputReducer.d.ts.map +1 -1
  50. package/dist/managers/inputReducer.js +519 -4
  51. package/dist/print-cli.d.ts.map +1 -1
  52. package/dist/print-cli.js +21 -9
  53. package/dist/reducers/backgroundTaskManagerReducer.d.ts +20 -0
  54. package/dist/reducers/backgroundTaskManagerReducer.d.ts.map +1 -1
  55. package/dist/reducers/backgroundTaskManagerReducer.js +83 -0
  56. package/dist/reducers/confirmationReducer.d.ts +11 -0
  57. package/dist/reducers/confirmationReducer.d.ts.map +1 -1
  58. package/dist/reducers/confirmationReducer.js +146 -0
  59. package/dist/reducers/helpSelectorReducer.d.ts +26 -0
  60. package/dist/reducers/helpSelectorReducer.d.ts.map +1 -0
  61. package/dist/reducers/helpSelectorReducer.js +52 -0
  62. package/dist/reducers/mcpManagerReducer.d.ts +22 -0
  63. package/dist/reducers/mcpManagerReducer.d.ts.map +1 -1
  64. package/dist/reducers/mcpManagerReducer.js +57 -0
  65. package/dist/reducers/pluginDetailReducer.d.ts +8 -0
  66. package/dist/reducers/pluginDetailReducer.d.ts.map +1 -1
  67. package/dist/reducers/pluginDetailReducer.js +32 -0
  68. package/dist/reducers/questionReducer.d.ts +21 -0
  69. package/dist/reducers/questionReducer.d.ts.map +1 -1
  70. package/dist/reducers/questionReducer.js +177 -0
  71. package/dist/reducers/rewindSelectorReducer.d.ts +22 -0
  72. package/dist/reducers/rewindSelectorReducer.d.ts.map +1 -0
  73. package/dist/reducers/rewindSelectorReducer.js +37 -0
  74. package/dist/reducers/selectorReducer.d.ts +22 -0
  75. package/dist/reducers/selectorReducer.d.ts.map +1 -0
  76. package/dist/reducers/selectorReducer.js +40 -0
  77. package/dist/utils/inputUtils.d.ts +18 -0
  78. package/dist/utils/inputUtils.d.ts.map +1 -0
  79. package/dist/utils/inputUtils.js +67 -0
  80. package/dist/utils/worktree.d.ts.map +1 -1
  81. package/dist/utils/worktree.js +1 -2
  82. package/package.json +2 -2
  83. package/src/acp/agent.ts +10 -0
  84. package/src/components/BackgroundTaskManager.tsx +19 -58
  85. package/src/components/CommandSelector.tsx +53 -44
  86. package/src/components/ConfirmationSelector.tsx +18 -202
  87. package/src/components/DiscoverView.tsx +21 -11
  88. package/src/components/FileSelector.tsx +40 -35
  89. package/src/components/HelpView.tsx +37 -43
  90. package/src/components/HistorySearch.tsx +29 -36
  91. package/src/components/InputBox.tsx +14 -7
  92. package/src/components/InstalledView.tsx +21 -11
  93. package/src/components/MarketplaceDetail.tsx +48 -24
  94. package/src/components/MarketplaceView.tsx +25 -10
  95. package/src/components/McpManager.tsx +29 -54
  96. package/src/components/ModelSelector.tsx +30 -23
  97. package/src/components/PluginDetail.tsx +50 -27
  98. package/src/components/RewindCommand.tsx +38 -32
  99. package/src/components/SessionSelector.tsx +23 -20
  100. package/src/components/StatusLine.tsx +10 -1
  101. package/src/components/WorktreeExitPrompt.tsx +23 -13
  102. package/src/contexts/useChat.tsx +13 -31
  103. package/src/hooks/useInputManager.ts +273 -141
  104. package/src/managers/inputHandlers.ts +1 -6
  105. package/src/managers/inputReducer.ts +675 -7
  106. package/src/print-cli.ts +20 -11
  107. package/src/reducers/backgroundTaskManagerReducer.ts +110 -1
  108. package/src/reducers/confirmationReducer.ts +173 -1
  109. package/src/reducers/helpSelectorReducer.ts +77 -0
  110. package/src/reducers/mcpManagerReducer.ts +82 -1
  111. package/src/reducers/pluginDetailReducer.ts +42 -1
  112. package/src/reducers/questionReducer.ts +204 -0
  113. package/src/reducers/rewindSelectorReducer.ts +54 -0
  114. package/src/reducers/selectorReducer.ts +57 -0
  115. package/src/utils/inputUtils.ts +94 -0
  116. package/src/utils/worktree.ts +1 -7
package/src/print-cli.ts CHANGED
@@ -71,18 +71,21 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
71
71
  isReasoning = false;
72
72
  isContent = false;
73
73
  // Assistant message started - no content to output yet
74
- process.stdout.write("\n");
75
74
  },
76
75
  onAssistantReasoningUpdated: (chunk: string) => {
77
76
  if (!isReasoning) {
78
- process.stdout.write("💭 Reasoning:\n");
77
+ process.stdout.write("\n💭 Reasoning:\n");
79
78
  isReasoning = true;
80
79
  }
81
80
  process.stdout.write(chunk);
82
81
  },
83
82
  onAssistantContentUpdated: (chunk: string) => {
84
- if (isReasoning && !isContent) {
85
- process.stdout.write("\n\n📝 Response:\n");
83
+ if (!isContent) {
84
+ if (isReasoning) {
85
+ process.stdout.write("\n\n📝 Response:\n");
86
+ } else {
87
+ process.stdout.write("\n");
88
+ }
86
89
  isContent = true;
87
90
  }
88
91
  // FR-001: Stream content updates for real-time display - output only the new chunk
@@ -93,24 +96,24 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
93
96
  onSubagentAssistantMessageAdded: (subagentId: string) => {
94
97
  subagentReasoningStates.set(subagentId, false);
95
98
  subagentContentStates.set(subagentId, false);
96
- process.stdout.write("\n ");
97
99
  },
98
100
  onSubagentAssistantReasoningUpdated: (
99
101
  subagentId: string,
100
102
  chunk: string,
101
103
  ) => {
102
104
  if (!subagentReasoningStates.get(subagentId)) {
103
- process.stdout.write("💭 Reasoning: ");
105
+ process.stdout.write("\n 💭 Reasoning: ");
104
106
  subagentReasoningStates.set(subagentId, true);
105
107
  }
106
108
  process.stdout.write(chunk);
107
109
  },
108
110
  onSubagentAssistantContentUpdated: (subagentId: string, chunk: string) => {
109
- if (
110
- subagentReasoningStates.get(subagentId) &&
111
- !subagentContentStates.get(subagentId)
112
- ) {
113
- process.stdout.write("\n 📝 Response: ");
111
+ if (!subagentContentStates.get(subagentId)) {
112
+ if (subagentReasoningStates.get(subagentId)) {
113
+ process.stdout.write("\n 📝 Response: ");
114
+ } else {
115
+ process.stdout.write("\n ");
116
+ }
114
117
  subagentContentStates.set(subagentId, true);
115
118
  }
116
119
  process.stdout.write(chunk);
@@ -161,6 +164,12 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
161
164
  // Send message if provided and not empty
162
165
  if (typeof message === "string" && message.trim() !== "") {
163
166
  await agent.sendMessage(message);
167
+ process.stdout.write("\n");
168
+ }
169
+
170
+ // Wait for running background tasks and subagents to complete
171
+ while (agent.hasRunningBackgroundWork) {
172
+ await new Promise((resolve) => setTimeout(resolve, 500));
164
173
  }
165
174
 
166
175
  // Display usage summary before exit
@@ -1,3 +1,5 @@
1
+ import { Key } from "ink";
2
+
1
3
  export interface Task {
2
4
  id: string;
3
5
  type: string;
@@ -16,21 +18,31 @@ export interface DetailOutput {
16
18
  outputPath?: string;
17
19
  }
18
20
 
21
+ export type PendingEffect =
22
+ | { type: "CANCEL" }
23
+ | { type: "STOP_TASK"; taskId: string };
24
+
19
25
  export interface BackgroundTaskManagerState {
20
26
  tasks: Task[];
21
27
  selectedIndex: number;
22
28
  viewMode: "list" | "detail";
23
29
  detailTaskId: string | null;
24
30
  detailOutput: DetailOutput | null;
31
+ pendingEffect: PendingEffect | null;
25
32
  }
26
33
 
27
34
  export type BackgroundTaskManagerAction =
28
35
  | { type: "SET_TASKS"; tasks: Task[] }
29
36
  | { type: "SELECT_INDEX"; index: number }
37
+ | { type: "MOVE_UP" }
38
+ | { type: "MOVE_DOWN" }
39
+ | { type: "SELECT_CURRENT" }
30
40
  | { type: "SET_VIEW_MODE"; mode: "list" | "detail" }
31
41
  | { type: "SET_DETAIL_TASK_ID"; id: string | null }
32
42
  | { type: "SET_DETAIL_OUTPUT"; output: DetailOutput | null }
33
- | { type: "RESET_DETAIL" };
43
+ | { type: "RESET_DETAIL" }
44
+ | { type: "HANDLE_KEY"; input: string; key: Key }
45
+ | { type: "CLEAR_PENDING_EFFECT" };
34
46
 
35
47
  export function backgroundTaskManagerReducer(
36
48
  state: BackgroundTaskManagerState,
@@ -41,6 +53,25 @@ export function backgroundTaskManagerReducer(
41
53
  return { ...state, tasks: action.tasks };
42
54
  case "SELECT_INDEX":
43
55
  return { ...state, selectedIndex: action.index };
56
+ case "MOVE_UP":
57
+ return { ...state, selectedIndex: Math.max(0, state.selectedIndex - 1) };
58
+ case "MOVE_DOWN":
59
+ return {
60
+ ...state,
61
+ selectedIndex: Math.min(
62
+ state.tasks.length > 0 ? state.tasks.length - 1 : 0,
63
+ state.selectedIndex + 1,
64
+ ),
65
+ };
66
+ case "SELECT_CURRENT":
67
+ if (state.tasks.length > 0 && state.selectedIndex < state.tasks.length) {
68
+ return {
69
+ ...state,
70
+ detailTaskId: state.tasks[state.selectedIndex].id,
71
+ viewMode: "detail",
72
+ };
73
+ }
74
+ return state;
44
75
  case "SET_VIEW_MODE":
45
76
  return { ...state, viewMode: action.mode };
46
77
  case "SET_DETAIL_TASK_ID":
@@ -54,6 +85,84 @@ export function backgroundTaskManagerReducer(
54
85
  detailTaskId: null,
55
86
  detailOutput: null,
56
87
  };
88
+ case "HANDLE_KEY": {
89
+ const { input, key } = action;
90
+ if (state.viewMode === "list") {
91
+ if (key.return) {
92
+ if (
93
+ state.tasks.length > 0 &&
94
+ state.selectedIndex < state.tasks.length
95
+ ) {
96
+ return {
97
+ ...state,
98
+ detailTaskId: state.tasks[state.selectedIndex].id,
99
+ viewMode: "detail",
100
+ };
101
+ }
102
+ return state;
103
+ }
104
+
105
+ if (key.escape) {
106
+ return { ...state, pendingEffect: { type: "CANCEL" } };
107
+ }
108
+
109
+ if (key.upArrow) {
110
+ return {
111
+ ...state,
112
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
113
+ };
114
+ }
115
+
116
+ if (key.downArrow) {
117
+ return {
118
+ ...state,
119
+ selectedIndex: Math.min(
120
+ state.tasks.length > 0 ? state.tasks.length - 1 : 0,
121
+ state.selectedIndex + 1,
122
+ ),
123
+ };
124
+ }
125
+
126
+ if (input === "k") {
127
+ if (
128
+ state.tasks.length > 0 &&
129
+ state.selectedIndex < state.tasks.length
130
+ ) {
131
+ const selectedTask = state.tasks[state.selectedIndex];
132
+ if (selectedTask.status === "running") {
133
+ return {
134
+ ...state,
135
+ pendingEffect: { type: "STOP_TASK", taskId: selectedTask.id },
136
+ };
137
+ }
138
+ }
139
+ return state;
140
+ }
141
+ } else if (state.viewMode === "detail") {
142
+ if (key.escape) {
143
+ return {
144
+ ...state,
145
+ viewMode: "list",
146
+ detailTaskId: null,
147
+ detailOutput: null,
148
+ };
149
+ }
150
+
151
+ if (input === "k" && state.detailTaskId) {
152
+ const task = state.tasks.find((t) => t.id === state.detailTaskId);
153
+ if (task && task.status === "running") {
154
+ return {
155
+ ...state,
156
+ pendingEffect: { type: "STOP_TASK", taskId: state.detailTaskId },
157
+ };
158
+ }
159
+ return state;
160
+ }
161
+ }
162
+ return state;
163
+ }
164
+ case "CLEAR_PENDING_EFFECT":
165
+ return { ...state, pendingEffect: null };
57
166
  default:
58
167
  return state;
59
168
  }
@@ -1,4 +1,10 @@
1
+ import { Key } from "ink";
1
2
  import type { PermissionDecision } from "wave-agent-sdk";
3
+ import {
4
+ BASH_TOOL_NAME,
5
+ EXIT_PLAN_MODE_TOOL_NAME,
6
+ ENTER_PLAN_MODE_TOOL_NAME,
7
+ } from "wave-agent-sdk";
2
8
 
3
9
  export interface ConfirmationState {
4
10
  selectedOption: "clear" | "auto" | "allow" | "alternative";
@@ -14,7 +20,17 @@ export type ConfirmationAction =
14
20
  | { type: "BACKSPACE" }
15
21
  | { type: "MOVE_CURSOR_LEFT" }
16
22
  | { type: "MOVE_CURSOR_RIGHT" }
17
- | { type: "CONFIRM"; decision: PermissionDecision };
23
+ | { type: "CONFIRM"; decision: PermissionDecision }
24
+ | { type: "CLEAR_DECISION" }
25
+ | {
26
+ type: "HANDLE_KEY";
27
+ input: string;
28
+ key: Key;
29
+ toolName: string;
30
+ toolInput?: Record<string, unknown>;
31
+ suggestedPrefix?: string;
32
+ hidePersistentOption?: boolean;
33
+ };
18
34
 
19
35
  export function confirmationReducer(
20
36
  state: ConfirmationState,
@@ -68,6 +84,162 @@ export function confirmationReducer(
68
84
  };
69
85
  case "CONFIRM":
70
86
  return { ...state, decision: action.decision };
87
+ case "CLEAR_DECISION":
88
+ return { ...state, decision: null };
89
+ case "HANDLE_KEY": {
90
+ const {
91
+ input,
92
+ key,
93
+ toolName,
94
+ toolInput,
95
+ suggestedPrefix,
96
+ hidePersistentOption,
97
+ } = action;
98
+
99
+ if (key.return) {
100
+ let decision: PermissionDecision | null = null;
101
+ if (state.selectedOption === "clear") {
102
+ decision = {
103
+ behavior: "allow",
104
+ newPermissionMode: "acceptEdits",
105
+ clearContext: true,
106
+ };
107
+ } else if (state.selectedOption === "allow") {
108
+ if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
109
+ decision = { behavior: "allow", newPermissionMode: "default" };
110
+ } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
111
+ decision = { behavior: "allow", newPermissionMode: "plan" };
112
+ } else {
113
+ decision = { behavior: "allow" };
114
+ }
115
+ } else if (state.selectedOption === "auto") {
116
+ if (toolName === BASH_TOOL_NAME) {
117
+ const command = (toolInput?.command as string) || "";
118
+ if (command.trim().startsWith("mkdir")) {
119
+ decision = {
120
+ behavior: "allow",
121
+ newPermissionMode: "acceptEdits",
122
+ };
123
+ } else {
124
+ const rule = suggestedPrefix
125
+ ? `Bash(${suggestedPrefix})`
126
+ : `Bash(${toolInput?.command})`;
127
+ decision = { behavior: "allow", newPermissionRule: rule };
128
+ }
129
+ } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
130
+ decision = { behavior: "allow", newPermissionMode: "plan" };
131
+ } else if (toolName.startsWith("mcp__")) {
132
+ decision = { behavior: "allow", newPermissionRule: toolName };
133
+ } else {
134
+ decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
135
+ }
136
+ } else if (state.alternativeText.trim()) {
137
+ decision = {
138
+ behavior: "deny",
139
+ message: state.alternativeText.trim(),
140
+ };
141
+ } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
142
+ decision = {
143
+ behavior: "deny",
144
+ message: "User chose not to enter plan mode",
145
+ };
146
+ }
147
+
148
+ if (decision) {
149
+ return { ...state, decision };
150
+ }
151
+ return state;
152
+ }
153
+
154
+ if (state.selectedOption === "alternative") {
155
+ if (key.leftArrow) {
156
+ return {
157
+ ...state,
158
+ alternativeCursorPosition: Math.max(
159
+ 0,
160
+ state.alternativeCursorPosition - 1,
161
+ ),
162
+ };
163
+ }
164
+ if (key.rightArrow) {
165
+ return {
166
+ ...state,
167
+ alternativeCursorPosition: Math.min(
168
+ state.alternativeText.length,
169
+ state.alternativeCursorPosition + 1,
170
+ ),
171
+ };
172
+ }
173
+ }
174
+
175
+ const availableOptions: ConfirmationState["selectedOption"][] = [];
176
+ if (toolName === EXIT_PLAN_MODE_TOOL_NAME) availableOptions.push("clear");
177
+ availableOptions.push("allow");
178
+ if (!hidePersistentOption) availableOptions.push("auto");
179
+ availableOptions.push("alternative");
180
+
181
+ if (key.upArrow) {
182
+ const currentIndex = availableOptions.indexOf(state.selectedOption);
183
+ if (currentIndex > 0) {
184
+ return {
185
+ ...state,
186
+ selectedOption: availableOptions[currentIndex - 1],
187
+ };
188
+ }
189
+ return state;
190
+ }
191
+
192
+ if (key.downArrow) {
193
+ const currentIndex = availableOptions.indexOf(state.selectedOption);
194
+ if (currentIndex < availableOptions.length - 1) {
195
+ return {
196
+ ...state,
197
+ selectedOption: availableOptions[currentIndex + 1],
198
+ };
199
+ }
200
+ return state;
201
+ }
202
+
203
+ if (key.tab) {
204
+ const currentIndex = availableOptions.indexOf(state.selectedOption);
205
+ const direction = key.shift ? -1 : 1;
206
+ let nextIndex = currentIndex + direction;
207
+ if (nextIndex < 0) nextIndex = availableOptions.length - 1;
208
+ if (nextIndex >= availableOptions.length) nextIndex = 0;
209
+ return { ...state, selectedOption: availableOptions[nextIndex] };
210
+ }
211
+
212
+ if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
213
+ const nextText =
214
+ state.alternativeText.slice(0, state.alternativeCursorPosition) +
215
+ input +
216
+ state.alternativeText.slice(state.alternativeCursorPosition);
217
+ return {
218
+ ...state,
219
+ selectedOption: "alternative",
220
+ alternativeText: nextText,
221
+ alternativeCursorPosition:
222
+ state.alternativeCursorPosition + input.length,
223
+ hasUserInput: true,
224
+ };
225
+ }
226
+
227
+ if (key.backspace || key.delete) {
228
+ if (state.alternativeCursorPosition <= 0) return state;
229
+ const nextText =
230
+ state.alternativeText.slice(0, state.alternativeCursorPosition - 1) +
231
+ state.alternativeText.slice(state.alternativeCursorPosition);
232
+ return {
233
+ ...state,
234
+ selectedOption: "alternative",
235
+ alternativeText: nextText,
236
+ alternativeCursorPosition: state.alternativeCursorPosition - 1,
237
+ hasUserInput: nextText.length > 0,
238
+ };
239
+ }
240
+
241
+ return state;
242
+ }
71
243
  default:
72
244
  return state;
73
245
  }
@@ -0,0 +1,77 @@
1
+ import { Key } from "ink";
2
+
3
+ export interface SelectorState {
4
+ selectedIndex: number;
5
+ activeTab: "general" | "commands" | "custom-commands";
6
+ pendingDecision: "select" | "insert" | "cancel" | "tab-switch" | null;
7
+ }
8
+
9
+ export type SelectorAction =
10
+ | { type: "MOVE_UP" }
11
+ | { type: "MOVE_DOWN"; maxIndex: number }
12
+ | { type: "RESET_INDEX" }
13
+ | { type: "SWITCH_TAB"; tabs: string[] }
14
+ | { type: "HANDLE_KEY"; key: Key; maxIndex: number; tabs: string[] }
15
+ | { type: "CLEAR_DECISION" };
16
+
17
+ export function helpSelectorReducer(
18
+ state: SelectorState,
19
+ action: SelectorAction,
20
+ ): SelectorState {
21
+ switch (action.type) {
22
+ case "MOVE_UP":
23
+ return { ...state, selectedIndex: Math.max(0, state.selectedIndex - 1) };
24
+ case "MOVE_DOWN":
25
+ return {
26
+ ...state,
27
+ selectedIndex: Math.min(action.maxIndex, state.selectedIndex + 1),
28
+ };
29
+ case "RESET_INDEX":
30
+ return { ...state, selectedIndex: 0 };
31
+ case "SWITCH_TAB": {
32
+ const currentIndex = action.tabs.indexOf(state.activeTab);
33
+ const nextIndex = (currentIndex + 1) % action.tabs.length;
34
+ return {
35
+ ...state,
36
+ activeTab: action.tabs[nextIndex] as
37
+ | "general"
38
+ | "commands"
39
+ | "custom-commands",
40
+ selectedIndex: 0,
41
+ };
42
+ }
43
+ case "HANDLE_KEY":
44
+ if (action.key.upArrow) {
45
+ return {
46
+ ...state,
47
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
48
+ };
49
+ }
50
+ if (action.key.downArrow) {
51
+ return {
52
+ ...state,
53
+ selectedIndex: Math.min(action.maxIndex, state.selectedIndex + 1),
54
+ };
55
+ }
56
+ if (action.key.tab) {
57
+ const currentIndex = action.tabs.indexOf(state.activeTab);
58
+ const nextIndex = (currentIndex + 1) % action.tabs.length;
59
+ return {
60
+ ...state,
61
+ activeTab: action.tabs[nextIndex] as
62
+ | "general"
63
+ | "commands"
64
+ | "custom-commands",
65
+ selectedIndex: 0,
66
+ };
67
+ }
68
+ if (action.key.escape) {
69
+ return { ...state, pendingDecision: "cancel" };
70
+ }
71
+ return state;
72
+ case "CLEAR_DECISION":
73
+ return { ...state, pendingDecision: null };
74
+ default:
75
+ return state;
76
+ }
77
+ }
@@ -1,12 +1,28 @@
1
+ import { Key } from "ink";
2
+
3
+ export type PendingEffect =
4
+ | { type: "CANCEL" }
5
+ | { type: "CONNECT_SERVER"; serverName: string }
6
+ | { type: "DISCONNECT_SERVER"; serverName: string };
7
+
1
8
  export interface McpManagerState {
2
9
  selectedIndex: number;
3
10
  viewMode: "list" | "detail";
11
+ pendingEffect: PendingEffect | null;
4
12
  }
5
13
 
6
14
  export type McpManagerAction =
7
15
  | { type: "MOVE_UP"; serverCount: number }
8
16
  | { type: "MOVE_DOWN"; serverCount: number }
9
- | { type: "SET_VIEW_MODE"; viewMode: "list" | "detail" };
17
+ | { type: "SET_VIEW_MODE"; viewMode: "list" | "detail" }
18
+ | {
19
+ type: "HANDLE_KEY";
20
+ input: string;
21
+ key: Key;
22
+ serverCount: number;
23
+ servers: Array<{ name: string; status: string }>;
24
+ }
25
+ | { type: "CLEAR_PENDING_EFFECT" };
10
26
 
11
27
  export function mcpManagerReducer(
12
28
  state: McpManagerState,
@@ -25,6 +41,71 @@ export function mcpManagerReducer(
25
41
  };
26
42
  case "SET_VIEW_MODE":
27
43
  return { ...state, viewMode: action.viewMode };
44
+ case "HANDLE_KEY": {
45
+ const { input, key, serverCount, servers } = action;
46
+
47
+ if (key.return) {
48
+ if (state.viewMode === "list") {
49
+ return { ...state, viewMode: "detail" };
50
+ }
51
+ return state;
52
+ }
53
+
54
+ if (key.escape) {
55
+ if (state.viewMode === "detail") {
56
+ return { ...state, viewMode: "list" };
57
+ } else {
58
+ return { ...state, pendingEffect: { type: "CANCEL" } };
59
+ }
60
+ }
61
+
62
+ if (key.upArrow) {
63
+ return {
64
+ ...state,
65
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
66
+ };
67
+ }
68
+
69
+ if (key.downArrow) {
70
+ return {
71
+ ...state,
72
+ selectedIndex: Math.min(serverCount - 1, state.selectedIndex + 1),
73
+ };
74
+ }
75
+
76
+ // Hotkeys for server actions
77
+ if (input === "c") {
78
+ const server = servers[state.selectedIndex];
79
+ if (
80
+ server &&
81
+ (server.status === "disconnected" || server.status === "error")
82
+ ) {
83
+ return {
84
+ ...state,
85
+ pendingEffect: { type: "CONNECT_SERVER", serverName: server.name },
86
+ };
87
+ }
88
+ return state;
89
+ }
90
+
91
+ if (input === "d") {
92
+ const server = servers[state.selectedIndex];
93
+ if (server && server.status === "connected") {
94
+ return {
95
+ ...state,
96
+ pendingEffect: {
97
+ type: "DISCONNECT_SERVER",
98
+ serverName: server.name,
99
+ },
100
+ };
101
+ }
102
+ return state;
103
+ }
104
+
105
+ return state;
106
+ }
107
+ case "CLEAR_PENDING_EFFECT":
108
+ return { ...state, pendingEffect: null };
28
109
  default:
29
110
  return state;
30
111
  }
@@ -1,6 +1,9 @@
1
+ import { Key } from "ink";
2
+
1
3
  export interface PluginDetailState {
2
4
  selectedScopeIndex: number;
3
5
  selectedActionIndex: number;
6
+ pendingDecision: "select" | "cancel" | null;
4
7
  }
5
8
 
6
9
  export type PluginDetailAction =
@@ -9,7 +12,9 @@ export type PluginDetailAction =
9
12
  | { type: "MOVE_SCOPE_UP"; maxIndex: number }
10
13
  | { type: "MOVE_SCOPE_DOWN"; maxIndex: number }
11
14
  | { type: "MOVE_ACTION_UP"; maxIndex: number }
12
- | { type: "MOVE_ACTION_DOWN"; maxIndex: number };
15
+ | { type: "MOVE_ACTION_DOWN"; maxIndex: number }
16
+ | { type: "HANDLE_KEY"; key: Key; maxIndex: number }
17
+ | { type: "CLEAR_DECISION" };
13
18
 
14
19
  export function pluginDetailReducer(
15
20
  state: PluginDetailState,
@@ -52,6 +57,42 @@ export function pluginDetailReducer(
52
57
  ? state.selectedActionIndex + 1
53
58
  : 0,
54
59
  };
60
+ case "HANDLE_KEY":
61
+ if (action.key.upArrow) {
62
+ return {
63
+ ...state,
64
+ selectedScopeIndex:
65
+ state.selectedScopeIndex > 0
66
+ ? state.selectedScopeIndex - 1
67
+ : action.maxIndex,
68
+ selectedActionIndex:
69
+ state.selectedActionIndex > 0
70
+ ? state.selectedActionIndex - 1
71
+ : action.maxIndex,
72
+ };
73
+ }
74
+ if (action.key.downArrow) {
75
+ return {
76
+ ...state,
77
+ selectedScopeIndex:
78
+ state.selectedScopeIndex < action.maxIndex
79
+ ? state.selectedScopeIndex + 1
80
+ : 0,
81
+ selectedActionIndex:
82
+ state.selectedActionIndex < action.maxIndex
83
+ ? state.selectedActionIndex + 1
84
+ : 0,
85
+ };
86
+ }
87
+ if (action.key.return) {
88
+ return { ...state, pendingDecision: "select" };
89
+ }
90
+ if (action.key.escape) {
91
+ return { ...state, pendingDecision: "cancel" };
92
+ }
93
+ return state;
94
+ case "CLEAR_DECISION":
95
+ return { ...state, pendingDecision: null };
55
96
  default:
56
97
  return state;
57
98
  }