wave-code 0.8.0 → 0.8.2

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 (46) hide show
  1. package/dist/components/ChatInterface.js +1 -1
  2. package/dist/components/CommandSelector.d.ts.map +1 -1
  3. package/dist/components/CommandSelector.js +1 -38
  4. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  5. package/dist/components/ConfirmationSelector.js +11 -3
  6. package/dist/components/HelpView.d.ts +2 -0
  7. package/dist/components/HelpView.d.ts.map +1 -1
  8. package/dist/components/HelpView.js +49 -5
  9. package/dist/components/InputBox.d.ts.map +1 -1
  10. package/dist/components/InputBox.js +1 -1
  11. package/dist/components/MessageList.d.ts +2 -2
  12. package/dist/components/MessageList.d.ts.map +1 -1
  13. package/dist/components/MessageList.js +5 -6
  14. package/dist/constants/commands.d.ts +3 -0
  15. package/dist/constants/commands.d.ts.map +1 -0
  16. package/dist/constants/commands.js +38 -0
  17. package/dist/contexts/useChat.d.ts.map +1 -1
  18. package/dist/contexts/useChat.js +2 -17
  19. package/dist/hooks/useInputManager.d.ts +7 -8
  20. package/dist/hooks/useInputManager.d.ts.map +1 -1
  21. package/dist/hooks/useInputManager.js +224 -232
  22. package/dist/managers/inputHandlers.d.ts +28 -0
  23. package/dist/managers/inputHandlers.d.ts.map +1 -0
  24. package/dist/managers/inputHandlers.js +378 -0
  25. package/dist/managers/inputReducer.d.ts +157 -0
  26. package/dist/managers/inputReducer.d.ts.map +1 -0
  27. package/dist/managers/inputReducer.js +242 -0
  28. package/dist/utils/highlightUtils.d.ts.map +1 -1
  29. package/dist/utils/highlightUtils.js +66 -42
  30. package/package.json +2 -2
  31. package/src/components/ChatInterface.tsx +1 -1
  32. package/src/components/CommandSelector.tsx +1 -40
  33. package/src/components/ConfirmationSelector.tsx +13 -3
  34. package/src/components/HelpView.tsx +129 -16
  35. package/src/components/InputBox.tsx +3 -1
  36. package/src/components/MessageList.tsx +5 -6
  37. package/src/constants/commands.ts +41 -0
  38. package/src/contexts/useChat.tsx +2 -17
  39. package/src/hooks/useInputManager.ts +352 -299
  40. package/src/managers/inputHandlers.ts +560 -0
  41. package/src/managers/inputReducer.ts +367 -0
  42. package/src/utils/highlightUtils.ts +66 -42
  43. package/dist/managers/InputManager.d.ts +0 -156
  44. package/dist/managers/InputManager.d.ts.map +0 -1
  45. package/dist/managers/InputManager.js +0 -749
  46. package/src/managers/InputManager.ts +0 -1024
@@ -0,0 +1,242 @@
1
+ export const initialState = {
2
+ inputText: "",
3
+ cursorPosition: 0,
4
+ showFileSelector: false,
5
+ atPosition: -1,
6
+ fileSearchQuery: "",
7
+ filteredFiles: [],
8
+ showCommandSelector: false,
9
+ slashPosition: -1,
10
+ commandSearchQuery: "",
11
+ showHistorySearch: false,
12
+ historySearchQuery: "",
13
+ longTextCounter: 0,
14
+ longTextMap: {},
15
+ attachedImages: [],
16
+ imageIdCounter: 1,
17
+ showBackgroundTaskManager: false,
18
+ showMcpManager: false,
19
+ showRewindManager: false,
20
+ showHelp: false,
21
+ showStatusCommand: false,
22
+ permissionMode: "default",
23
+ selectorJustUsed: false,
24
+ isPasting: false,
25
+ pasteBuffer: "",
26
+ initialPasteCursorPosition: 0,
27
+ };
28
+ export function inputReducer(state, action) {
29
+ switch (action.type) {
30
+ case "SET_INPUT_TEXT":
31
+ return { ...state, inputText: action.payload };
32
+ case "SET_CURSOR_POSITION":
33
+ return {
34
+ ...state,
35
+ cursorPosition: Math.max(0, Math.min(state.inputText.length, action.payload)),
36
+ };
37
+ case "INSERT_TEXT": {
38
+ const beforeCursor = state.inputText.substring(0, state.cursorPosition);
39
+ const afterCursor = state.inputText.substring(state.cursorPosition);
40
+ const newText = beforeCursor + action.payload + afterCursor;
41
+ const newCursorPosition = state.cursorPosition + action.payload.length;
42
+ return {
43
+ ...state,
44
+ inputText: newText,
45
+ cursorPosition: newCursorPosition,
46
+ };
47
+ }
48
+ case "DELETE_CHAR": {
49
+ if (state.cursorPosition > 0) {
50
+ const beforeCursor = state.inputText.substring(0, state.cursorPosition - 1);
51
+ const afterCursor = state.inputText.substring(state.cursorPosition);
52
+ const newText = beforeCursor + afterCursor;
53
+ const newCursorPosition = state.cursorPosition - 1;
54
+ return {
55
+ ...state,
56
+ inputText: newText,
57
+ cursorPosition: newCursorPosition,
58
+ };
59
+ }
60
+ return state;
61
+ }
62
+ case "MOVE_CURSOR": {
63
+ const newCursorPosition = Math.max(0, Math.min(state.inputText.length, state.cursorPosition + action.payload));
64
+ return { ...state, cursorPosition: newCursorPosition };
65
+ }
66
+ case "ACTIVATE_FILE_SELECTOR":
67
+ return {
68
+ ...state,
69
+ showFileSelector: true,
70
+ atPosition: action.payload,
71
+ fileSearchQuery: "",
72
+ filteredFiles: [],
73
+ };
74
+ case "SET_FILE_SEARCH_QUERY":
75
+ return { ...state, fileSearchQuery: action.payload };
76
+ case "SET_FILTERED_FILES":
77
+ return { ...state, filteredFiles: action.payload };
78
+ case "CANCEL_FILE_SELECTOR":
79
+ return {
80
+ ...state,
81
+ showFileSelector: false,
82
+ atPosition: -1,
83
+ fileSearchQuery: "",
84
+ filteredFiles: [],
85
+ selectorJustUsed: true,
86
+ };
87
+ case "ACTIVATE_COMMAND_SELECTOR":
88
+ return {
89
+ ...state,
90
+ showCommandSelector: true,
91
+ slashPosition: action.payload,
92
+ commandSearchQuery: "",
93
+ };
94
+ case "SET_COMMAND_SEARCH_QUERY":
95
+ return { ...state, commandSearchQuery: action.payload };
96
+ case "CANCEL_COMMAND_SELECTOR":
97
+ return {
98
+ ...state,
99
+ showCommandSelector: false,
100
+ slashPosition: -1,
101
+ commandSearchQuery: "",
102
+ selectorJustUsed: true,
103
+ };
104
+ case "ACTIVATE_HISTORY_SEARCH":
105
+ return {
106
+ ...state,
107
+ showHistorySearch: true,
108
+ historySearchQuery: "",
109
+ };
110
+ case "SET_HISTORY_SEARCH_QUERY":
111
+ return { ...state, historySearchQuery: action.payload };
112
+ case "CANCEL_HISTORY_SEARCH":
113
+ return {
114
+ ...state,
115
+ showHistorySearch: false,
116
+ historySearchQuery: "",
117
+ selectorJustUsed: true,
118
+ };
119
+ case "ADD_IMAGE": {
120
+ const newImage = {
121
+ id: state.imageIdCounter,
122
+ path: action.payload.path,
123
+ mimeType: action.payload.mimeType,
124
+ };
125
+ return {
126
+ ...state,
127
+ attachedImages: [...state.attachedImages, newImage],
128
+ imageIdCounter: state.imageIdCounter + 1,
129
+ };
130
+ }
131
+ case "REMOVE_IMAGE":
132
+ return {
133
+ ...state,
134
+ attachedImages: state.attachedImages.filter((img) => img.id !== action.payload),
135
+ };
136
+ case "CLEAR_IMAGES":
137
+ return { ...state, attachedImages: [] };
138
+ case "SET_SHOW_BACKGROUND_TASK_MANAGER":
139
+ return {
140
+ ...state,
141
+ showBackgroundTaskManager: action.payload,
142
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
143
+ };
144
+ case "SET_SHOW_MCP_MANAGER":
145
+ return {
146
+ ...state,
147
+ showMcpManager: action.payload,
148
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
149
+ };
150
+ case "SET_SHOW_REWIND_MANAGER":
151
+ return {
152
+ ...state,
153
+ showRewindManager: action.payload,
154
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
155
+ };
156
+ case "SET_SHOW_HELP":
157
+ return {
158
+ ...state,
159
+ showHelp: action.payload,
160
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
161
+ };
162
+ case "SET_SHOW_STATUS_COMMAND":
163
+ return {
164
+ ...state,
165
+ showStatusCommand: action.payload,
166
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
167
+ };
168
+ case "SET_PERMISSION_MODE":
169
+ return { ...state, permissionMode: action.payload };
170
+ case "SET_SELECTOR_JUST_USED":
171
+ return { ...state, selectorJustUsed: action.payload };
172
+ case "COMPRESS_AND_INSERT_TEXT": {
173
+ let textToInsert = action.payload;
174
+ let newLongTextCounter = state.longTextCounter;
175
+ const newLongTextMap = { ...state.longTextMap };
176
+ if (textToInsert.length > 200) {
177
+ newLongTextCounter += 1;
178
+ const compressedLabel = `[LongText#${newLongTextCounter}]`;
179
+ newLongTextMap[compressedLabel] = textToInsert;
180
+ textToInsert = compressedLabel;
181
+ }
182
+ const beforeCursor = state.inputText.substring(0, state.cursorPosition);
183
+ const afterCursor = state.inputText.substring(state.cursorPosition);
184
+ const newText = beforeCursor + textToInsert + afterCursor;
185
+ const newCursorPosition = state.cursorPosition + textToInsert.length;
186
+ return {
187
+ ...state,
188
+ inputText: newText,
189
+ cursorPosition: newCursorPosition,
190
+ longTextCounter: newLongTextCounter,
191
+ longTextMap: newLongTextMap,
192
+ };
193
+ }
194
+ case "CLEAR_LONG_TEXT_MAP":
195
+ return { ...state, longTextMap: {} };
196
+ case "CLEAR_INPUT":
197
+ return {
198
+ ...state,
199
+ inputText: "",
200
+ cursorPosition: 0,
201
+ };
202
+ case "START_PASTE":
203
+ return {
204
+ ...state,
205
+ isPasting: true,
206
+ pasteBuffer: action.payload.buffer,
207
+ initialPasteCursorPosition: action.payload.cursorPosition,
208
+ };
209
+ case "APPEND_PASTE_BUFFER":
210
+ return {
211
+ ...state,
212
+ pasteBuffer: state.pasteBuffer + action.payload,
213
+ };
214
+ case "END_PASTE":
215
+ return {
216
+ ...state,
217
+ isPasting: false,
218
+ pasteBuffer: "",
219
+ };
220
+ case "ADD_IMAGE_AND_INSERT_PLACEHOLDER": {
221
+ const newImage = {
222
+ id: state.imageIdCounter,
223
+ path: action.payload.path,
224
+ mimeType: action.payload.mimeType,
225
+ };
226
+ const placeholder = `[Image #${newImage.id}]`;
227
+ const beforeCursor = state.inputText.substring(0, state.cursorPosition);
228
+ const afterCursor = state.inputText.substring(state.cursorPosition);
229
+ const newText = beforeCursor + placeholder + afterCursor;
230
+ const newCursorPosition = state.cursorPosition + placeholder.length;
231
+ return {
232
+ ...state,
233
+ attachedImages: [...state.attachedImages, newImage],
234
+ imageIdCounter: state.imageIdCounter + 1,
235
+ inputText: newText,
236
+ cursorPosition: newCursorPosition,
237
+ };
238
+ }
239
+ default:
240
+ return state;
241
+ }
242
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"highlightUtils.d.ts","sourceRoot":"","sources":["../../src/utils/highlightUtils.ts"],"names":[],"mappings":"AA6DA,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAcvE"}
1
+ {"version":3,"file":"highlightUtils.d.ts","sourceRoot":"","sources":["../../src/utils/highlightUtils.ts"],"names":[],"mappings":"AA6DA,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAsCvE"}
@@ -1,48 +1,48 @@
1
- import hljs from 'highlight.js';
2
- import { parse, HTMLElement, TextNode } from 'node-html-parser';
3
- import chalk from 'chalk';
1
+ import hljs from "highlight.js";
2
+ import { parse, HTMLElement, TextNode } from "node-html-parser";
3
+ import chalk from "chalk";
4
4
  const theme = {
5
- 'hljs-keyword': chalk.blue,
6
- 'hljs-built_in': chalk.cyan,
7
- 'hljs-type': chalk.cyan,
8
- 'hljs-literal': chalk.magenta,
9
- 'hljs-number': chalk.magenta,
10
- 'hljs-operator': chalk.white,
11
- 'hljs-punctuation': chalk.white,
12
- 'hljs-property': chalk.yellow,
13
- 'hljs-attr': chalk.yellow,
14
- 'hljs-variable': chalk.white,
15
- 'hljs-template-variable': chalk.white,
16
- 'hljs-string': chalk.green,
17
- 'hljs-char': chalk.green,
18
- 'hljs-comment': chalk.gray,
19
- 'hljs-doctag': chalk.gray,
20
- 'hljs-function': chalk.yellow,
21
- 'hljs-title': chalk.yellow,
22
- 'hljs-params': chalk.white,
23
- 'hljs-tag': chalk.blue,
24
- 'hljs-name': chalk.blue,
25
- 'hljs-selector-tag': chalk.blue,
26
- 'hljs-selector-id': chalk.blue,
27
- 'hljs-selector-class': chalk.blue,
28
- 'hljs-selector-attr': chalk.blue,
29
- 'hljs-selector-pseudo': chalk.blue,
30
- 'hljs-subst': chalk.white,
31
- 'hljs-section': chalk.blue.bold,
32
- 'hljs-bullet': chalk.magenta,
33
- 'hljs-emphasis': chalk.italic,
34
- 'hljs-strong': chalk.bold,
35
- 'hljs-addition': chalk.green,
36
- 'hljs-deletion': chalk.red,
37
- 'hljs-link': chalk.blue.underline,
5
+ "hljs-keyword": chalk.blue,
6
+ "hljs-built_in": chalk.cyan,
7
+ "hljs-type": chalk.cyan,
8
+ "hljs-literal": chalk.magenta,
9
+ "hljs-number": chalk.magenta,
10
+ "hljs-operator": chalk.white,
11
+ "hljs-punctuation": chalk.white,
12
+ "hljs-property": chalk.yellow,
13
+ "hljs-attr": chalk.yellow,
14
+ "hljs-variable": chalk.white,
15
+ "hljs-template-variable": chalk.white,
16
+ "hljs-string": chalk.green,
17
+ "hljs-char": chalk.green,
18
+ "hljs-comment": chalk.gray,
19
+ "hljs-doctag": chalk.gray,
20
+ "hljs-function": chalk.yellow,
21
+ "hljs-title": chalk.yellow,
22
+ "hljs-params": chalk.white,
23
+ "hljs-tag": chalk.blue,
24
+ "hljs-name": chalk.blue,
25
+ "hljs-selector-tag": chalk.blue,
26
+ "hljs-selector-id": chalk.blue,
27
+ "hljs-selector-class": chalk.blue,
28
+ "hljs-selector-attr": chalk.blue,
29
+ "hljs-selector-pseudo": chalk.blue,
30
+ "hljs-subst": chalk.white,
31
+ "hljs-section": chalk.blue.bold,
32
+ "hljs-bullet": chalk.magenta,
33
+ "hljs-emphasis": chalk.italic,
34
+ "hljs-strong": chalk.bold,
35
+ "hljs-addition": chalk.green,
36
+ "hljs-deletion": chalk.red,
37
+ "hljs-link": chalk.blue.underline,
38
38
  };
39
39
  function nodeToAnsi(node) {
40
40
  if (node instanceof TextNode) {
41
41
  return node.text;
42
42
  }
43
43
  if (node instanceof HTMLElement) {
44
- const content = node.childNodes.map(nodeToAnsi).join('');
45
- const classes = node.getAttribute('class')?.split(/\s+/) || [];
44
+ const content = node.childNodes.map(nodeToAnsi).join("");
45
+ const classes = node.getAttribute("class")?.split(/\s+/) || [];
46
46
  for (const className of classes) {
47
47
  if (theme[className]) {
48
48
  return theme[className](content);
@@ -50,18 +50,42 @@ function nodeToAnsi(node) {
50
50
  }
51
51
  return content;
52
52
  }
53
- return '';
53
+ return "";
54
54
  }
55
55
  export function highlightToAnsi(code, language) {
56
56
  if (!code) {
57
- return '';
57
+ return "";
58
58
  }
59
59
  try {
60
60
  const highlighted = language
61
61
  ? hljs.highlight(code, { language }).value
62
- : hljs.highlightAuto(code).value;
62
+ : hljs.highlightAuto(code, [
63
+ "javascript",
64
+ "typescript",
65
+ "bash",
66
+ "json",
67
+ "markdown",
68
+ "python",
69
+ "yaml",
70
+ "html",
71
+ "css",
72
+ "sql",
73
+ "xml",
74
+ "rust",
75
+ "go",
76
+ "java",
77
+ "cpp",
78
+ "c",
79
+ "csharp",
80
+ "php",
81
+ "ruby",
82
+ "swift",
83
+ "kotlin",
84
+ "toml",
85
+ "ini",
86
+ ]).value;
63
87
  const root = parse(highlighted);
64
- return root.childNodes.map(nodeToAnsi).join('');
88
+ return root.childNodes.map(nodeToAnsi).join("");
65
89
  }
66
90
  catch {
67
91
  return code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "react": "^19.2.4",
40
40
  "react-dom": "19.2.4",
41
41
  "yargs": "^17.7.2",
42
- "wave-agent-sdk": "0.8.0"
42
+ "wave-agent-sdk": "0.8.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/react": "^19.1.8",
@@ -95,7 +95,7 @@ export const ChatInterface: React.FC = () => {
95
95
  <MessageList
96
96
  messages={messages}
97
97
  isExpanded={isExpanded}
98
- hideDynamicBlocks={isConfirmationVisible}
98
+ forceStatic={isConfirmationVisible && isConfirmationTooTall}
99
99
  version={version}
100
100
  workdir={workdir}
101
101
  model={model}
@@ -1,46 +1,7 @@
1
1
  import React, { useState } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import type { SlashCommand } from "wave-agent-sdk";
4
-
5
- const AVAILABLE_COMMANDS: SlashCommand[] = [
6
- {
7
- id: "clear",
8
- name: "clear",
9
- description: "Clear the chat session and terminal",
10
- handler: () => {}, // Handler here won't be used, actual processing is in the hook
11
- },
12
- {
13
- id: "tasks",
14
- name: "tasks",
15
- description: "View and manage background tasks (shells and subagents)",
16
- handler: () => {}, // Handler here won't be used, actual processing is in the hook
17
- },
18
- {
19
- id: "mcp",
20
- name: "mcp",
21
- description: "View and manage MCP servers",
22
- handler: () => {}, // Handler here won't be used, actual processing is in the hook
23
- },
24
- {
25
- id: "rewind",
26
- name: "rewind",
27
- description:
28
- "Revert conversation and file changes to a previous checkpoint",
29
- handler: () => {}, // Handler here won't be used, actual processing is in the hook
30
- },
31
- {
32
- id: "help",
33
- name: "help",
34
- description: "Show help and key bindings",
35
- handler: () => {}, // Handler here won't be used, actual processing is in the hook
36
- },
37
- {
38
- id: "status",
39
- name: "status",
40
- description: "Show agent status and configuration",
41
- handler: () => {}, // Handler here won't be used, actual processing is in the hook
42
- },
43
- ];
4
+ import { AVAILABLE_COMMANDS } from "../constants/commands.js";
44
5
 
45
6
  export interface CommandSelectorProps {
46
7
  searchQuery: string;
@@ -1,4 +1,4 @@
1
- import React, { useLayoutEffect, useRef, useState } from "react";
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import { Box, Text, useInput, useStdout, measureElement } from "ink";
3
3
  import type { PermissionDecision, AskUserQuestionInput } from "wave-agent-sdk";
4
4
  import {
@@ -81,6 +81,16 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
81
81
  >,
82
82
  });
83
83
 
84
+ const pendingDecisionRef = useRef<PermissionDecision | null>(null);
85
+
86
+ useEffect(() => {
87
+ if (pendingDecisionRef.current) {
88
+ const decision = pendingDecisionRef.current;
89
+ pendingDecisionRef.current = null;
90
+ onDecision(decision);
91
+ }
92
+ });
93
+
84
94
  const questions =
85
95
  (toolInput as unknown as AskUserQuestionInput)?.questions || [];
86
96
  const currentQuestion = questions[questionState.currentQuestionIndex];
@@ -204,10 +214,10 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
204
214
  );
205
215
  if (!allAnswered) return prev;
206
216
 
207
- onDecision({
217
+ pendingDecisionRef.current = {
208
218
  behavior: "allow",
209
219
  message: JSON.stringify(finalAnswers),
210
- });
220
+ };
211
221
  return {
212
222
  ...prev,
213
223
  userAnswers: finalAnswers,
@@ -1,14 +1,57 @@
1
- import React from "react";
1
+ import React, { useState } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
+ import type { SlashCommand } from "wave-agent-sdk";
4
+ import { AVAILABLE_COMMANDS } from "../constants/commands.js";
3
5
 
4
6
  export interface HelpViewProps {
5
7
  onCancel: () => void;
8
+ commands?: SlashCommand[];
6
9
  }
7
10
 
8
- export const HelpView: React.FC<HelpViewProps> = ({ onCancel }) => {
11
+ export const HelpView: React.FC<HelpViewProps> = ({
12
+ onCancel,
13
+ commands = [],
14
+ }) => {
15
+ const [activeTab, setActiveTab] = useState<
16
+ "general" | "commands" | "custom-commands"
17
+ >("general");
18
+ const [selectedIndex, setSelectedIndex] = useState(0);
19
+ const MAX_VISIBLE_ITEMS = 10;
20
+
21
+ const tabs: ("general" | "commands" | "custom-commands")[] = [
22
+ "general",
23
+ "commands",
24
+ ];
25
+ if (commands.length > 0) {
26
+ tabs.push("custom-commands");
27
+ }
28
+
9
29
  useInput((_, key) => {
10
- if (key.escape || key.return) {
30
+ if (key.escape) {
11
31
  onCancel();
32
+ return;
33
+ }
34
+
35
+ if (key.tab) {
36
+ setActiveTab((prev) => {
37
+ const currentIndex = tabs.indexOf(prev);
38
+ const nextIndex = (currentIndex + 1) % tabs.length;
39
+ return tabs[nextIndex];
40
+ });
41
+ setSelectedIndex(0);
42
+ return;
43
+ }
44
+
45
+ if (activeTab === "commands" || activeTab === "custom-commands") {
46
+ const currentCommands =
47
+ activeTab === "commands" ? AVAILABLE_COMMANDS : commands;
48
+ if (key.upArrow) {
49
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
50
+ } else if (key.downArrow) {
51
+ setSelectedIndex((prev) =>
52
+ Math.min(currentCommands.length - 1, prev + 1),
53
+ );
54
+ }
12
55
  }
13
56
  });
14
57
 
@@ -21,14 +64,35 @@ export const HelpView: React.FC<HelpViewProps> = ({ onCancel }) => {
21
64
  { key: "Ctrl+B", description: "Background current task" },
22
65
  { key: "Ctrl+V", description: "Paste image" },
23
66
  { key: "Shift+Tab", description: "Cycle permission mode" },
24
- { key: "/status", description: "Show agent status and configuration" },
25
- { key: "/clear", description: "Clear the chat session and terminal" },
26
67
  {
27
68
  key: "Esc",
28
69
  description: "Interrupt AI or command / Cancel selector / Close help",
29
70
  },
30
71
  ];
31
72
 
73
+ // Calculate visible window for commands
74
+ const currentCommands =
75
+ activeTab === "commands" ? AVAILABLE_COMMANDS : commands;
76
+ const startIndex = Math.max(
77
+ 0,
78
+ Math.min(
79
+ selectedIndex - Math.floor(MAX_VISIBLE_ITEMS / 2),
80
+ Math.max(0, currentCommands.length - MAX_VISIBLE_ITEMS),
81
+ ),
82
+ );
83
+ const visibleCommands = currentCommands.slice(
84
+ startIndex,
85
+ startIndex + MAX_VISIBLE_ITEMS,
86
+ );
87
+
88
+ const footerText = [
89
+ "Tab switch",
90
+ activeTab !== "general" && "↑↓ navigate",
91
+ "Esc close",
92
+ ]
93
+ .filter(Boolean)
94
+ .join(" • ");
95
+
32
96
  return (
33
97
  <Box
34
98
  flexDirection="column"
@@ -37,24 +101,73 @@ export const HelpView: React.FC<HelpViewProps> = ({ onCancel }) => {
37
101
  borderLeft={false}
38
102
  borderRight={false}
39
103
  paddingX={1}
104
+ width="100%"
40
105
  >
41
- <Box marginBottom={1}>
42
- <Text color="cyan" bold underline>
43
- Help & Key Bindings
106
+ <Box marginBottom={1} gap={2}>
107
+ <Text
108
+ color={activeTab === "general" ? "cyan" : "gray"}
109
+ bold
110
+ underline={activeTab === "general"}
111
+ >
112
+ General
113
+ </Text>
114
+ <Text
115
+ color={activeTab === "commands" ? "cyan" : "gray"}
116
+ bold
117
+ underline={activeTab === "commands"}
118
+ >
119
+ Commands
44
120
  </Text>
121
+ {commands.length > 0 && (
122
+ <Text
123
+ color={activeTab === "custom-commands" ? "cyan" : "gray"}
124
+ bold
125
+ underline={activeTab === "custom-commands"}
126
+ >
127
+ Custom Commands
128
+ </Text>
129
+ )}
45
130
  </Box>
46
131
 
47
- {helpItems.map((item, index) => (
48
- <Box key={index}>
49
- <Box width={20}>
50
- <Text color="yellow">{item.key}</Text>
51
- </Box>
52
- <Text color="white">{item.description}</Text>
132
+ {activeTab === "general" ? (
133
+ <Box flexDirection="column">
134
+ {helpItems.map((item, index) => (
135
+ <Box key={index}>
136
+ <Box width={20}>
137
+ <Text color="yellow">{item.key}</Text>
138
+ </Box>
139
+ <Text color="white">{item.description}</Text>
140
+ </Box>
141
+ ))}
142
+ </Box>
143
+ ) : (
144
+ <Box flexDirection="column">
145
+ {visibleCommands.map((command, index) => {
146
+ const actualIndex = startIndex + index;
147
+ const isSelected = actualIndex === selectedIndex;
148
+ return (
149
+ <Box key={command.id} flexDirection="column">
150
+ <Text
151
+ color={isSelected ? "black" : "white"}
152
+ backgroundColor={isSelected ? "cyan" : undefined}
153
+ >
154
+ {isSelected ? "▶ " : " "}/{command.id}
155
+ </Text>
156
+ {isSelected && (
157
+ <Box marginLeft={4}>
158
+ <Text color="gray" dimColor>
159
+ {command.description}
160
+ </Text>
161
+ </Box>
162
+ )}
163
+ </Box>
164
+ );
165
+ })}
53
166
  </Box>
54
- ))}
167
+ )}
55
168
 
56
169
  <Box marginTop={1}>
57
- <Text dimColor>Press Esc or Enter to close</Text>
170
+ <Text dimColor>{footerText}</Text>
58
171
  </Box>
59
172
  </Box>
60
173
  );