wave-code 0.11.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/acp/agent.d.ts.map +1 -1
  2. package/dist/acp/agent.js +9 -0
  3. package/dist/commands/update.d.ts +2 -0
  4. package/dist/commands/update.d.ts.map +1 -0
  5. package/dist/commands/update.js +97 -0
  6. package/dist/components/App.js +1 -1
  7. package/dist/components/BtwDisplay.d.ts +8 -0
  8. package/dist/components/BtwDisplay.d.ts.map +1 -0
  9. package/dist/components/BtwDisplay.js +9 -0
  10. package/dist/components/ChatInterface.d.ts.map +1 -1
  11. package/dist/components/ChatInterface.js +3 -5
  12. package/dist/components/ConfirmationDetails.d.ts.map +1 -1
  13. package/dist/components/ConfirmationDetails.js +7 -1
  14. package/dist/components/InputBox.d.ts.map +1 -1
  15. package/dist/components/InputBox.js +18 -4
  16. package/dist/components/MessageBlockItem.d.ts.map +1 -1
  17. package/dist/components/MessageBlockItem.js +2 -1
  18. package/dist/components/MessageList.d.ts +1 -2
  19. package/dist/components/MessageList.d.ts.map +1 -1
  20. package/dist/components/MessageList.js +9 -6
  21. package/dist/components/ModelSelector.d.ts +9 -0
  22. package/dist/components/ModelSelector.d.ts.map +1 -0
  23. package/dist/components/ModelSelector.js +34 -0
  24. package/dist/components/RewindCommand.js +2 -2
  25. package/dist/components/SlashDisplay.d.ts +9 -0
  26. package/dist/components/SlashDisplay.d.ts.map +1 -0
  27. package/dist/components/SlashDisplay.js +20 -0
  28. package/dist/components/StatusLine.d.ts +8 -0
  29. package/dist/components/StatusLine.d.ts.map +1 -0
  30. package/dist/components/StatusLine.js +5 -0
  31. package/dist/constants/commands.d.ts.map +1 -1
  32. package/dist/constants/commands.js +12 -0
  33. package/dist/contexts/useChat.d.ts +8 -0
  34. package/dist/contexts/useChat.d.ts.map +1 -1
  35. package/dist/contexts/useChat.js +61 -1
  36. package/dist/hooks/useInputManager.d.ts +5 -0
  37. package/dist/hooks/useInputManager.d.ts.map +1 -1
  38. package/dist/hooks/useInputManager.js +58 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -0
  41. package/dist/managers/inputHandlers.d.ts +1 -1
  42. package/dist/managers/inputHandlers.d.ts.map +1 -1
  43. package/dist/managers/inputHandlers.js +74 -5
  44. package/dist/managers/inputReducer.d.ts +22 -0
  45. package/dist/managers/inputReducer.d.ts.map +1 -1
  46. package/dist/managers/inputReducer.js +23 -0
  47. package/dist/utils/version.d.ts +2 -0
  48. package/dist/utils/version.d.ts.map +1 -0
  49. package/dist/utils/version.js +4 -0
  50. package/package.json +4 -2
  51. package/src/acp/agent.ts +10 -0
  52. package/src/commands/update.ts +116 -0
  53. package/src/components/App.tsx +1 -1
  54. package/src/components/BtwDisplay.tsx +33 -0
  55. package/src/components/ChatInterface.tsx +25 -29
  56. package/src/components/ConfirmationDetails.tsx +14 -0
  57. package/src/components/InputBox.tsx +47 -17
  58. package/src/components/MessageBlockItem.tsx +5 -5
  59. package/src/components/MessageList.tsx +9 -10
  60. package/src/components/ModelSelector.tsx +109 -0
  61. package/src/components/RewindCommand.tsx +2 -2
  62. package/src/components/SlashDisplay.tsx +70 -0
  63. package/src/components/StatusLine.tsx +36 -0
  64. package/src/constants/commands.ts +12 -0
  65. package/src/contexts/useChat.tsx +82 -1
  66. package/src/hooks/useInputManager.ts +69 -0
  67. package/src/index.ts +9 -0
  68. package/src/managers/inputHandlers.ts +92 -4
  69. package/src/managers/inputReducer.ts +41 -1
  70. package/src/utils/version.ts +8 -0
@@ -0,0 +1,116 @@
1
+ import { spawnSync } from "child_process";
2
+ import { readFileSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import https from "https";
6
+ import chalk from "chalk";
7
+ import { isUpdateAvailable } from "../utils/version.js";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const packageJsonPath = path.resolve(__dirname, "../../package.json");
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
12
+ const currentVersion = packageJson.version;
13
+
14
+ async function getLatestVersion(): Promise<string> {
15
+ return new Promise((resolve, reject) => {
16
+ https
17
+ .get("https://registry.npmjs.org/wave-code/latest", (res) => {
18
+ let data = "";
19
+ res.on("data", (chunk) => {
20
+ data += chunk;
21
+ });
22
+ res.on("end", () => {
23
+ try {
24
+ const json = JSON.parse(data);
25
+ resolve(json.version);
26
+ } catch {
27
+ reject(new Error("Failed to parse npm registry response"));
28
+ }
29
+ });
30
+ })
31
+ .on("error", (err) => {
32
+ reject(err);
33
+ });
34
+ });
35
+ }
36
+
37
+ function detectPackageManager(): "npm" | "pnpm" | "yarn" {
38
+ // Check if wave-code is installed globally with pnpm
39
+ const pnpmList = spawnSync("pnpm", ["list", "-g", "wave-code"], {
40
+ encoding: "utf-8",
41
+ });
42
+ if (pnpmList.status === 0 && pnpmList.stdout?.includes("wave-code")) {
43
+ return "pnpm";
44
+ }
45
+
46
+ // Check if wave-code is installed globally with yarn
47
+ const yarnList = spawnSync("yarn", ["global", "list"], { encoding: "utf-8" });
48
+ if (yarnList.status === 0 && yarnList.stdout?.includes("wave-code")) {
49
+ return "yarn";
50
+ }
51
+
52
+ // Default to npm
53
+ return "npm";
54
+ }
55
+
56
+ export async function updateCommand() {
57
+ console.log(chalk.blue(`Checking for updates...`));
58
+ console.log(chalk.dim(`Current version: ${currentVersion}`));
59
+
60
+ try {
61
+ const latestVersion = await getLatestVersion();
62
+ console.log(chalk.dim(`Latest version: ${latestVersion}`));
63
+
64
+ if (!isUpdateAvailable(currentVersion, latestVersion)) {
65
+ console.log(chalk.green("WAVE Code is already up to date!"));
66
+ process.exit(0);
67
+ }
68
+
69
+ console.log(
70
+ chalk.yellow(`A new version of WAVE Code is available: ${latestVersion}`),
71
+ );
72
+
73
+ const packageManager = detectPackageManager();
74
+ let updateCmd: string;
75
+ let args: string[];
76
+
77
+ if (packageManager === "pnpm") {
78
+ updateCmd = "pnpm";
79
+ args = ["add", "-g", "wave-code@latest"];
80
+ } else if (packageManager === "yarn") {
81
+ updateCmd = "yarn";
82
+ args = ["global", "add", "wave-code@latest"];
83
+ } else {
84
+ updateCmd = "npm";
85
+ args = ["install", "-g", "wave-code@latest"];
86
+ }
87
+
88
+ console.log(chalk.blue(`Updating WAVE Code using ${packageManager}...`));
89
+ console.log(chalk.dim(`Running: ${updateCmd} ${args.join(" ")}`));
90
+
91
+ const result = spawnSync(updateCmd, args, { stdio: "inherit" });
92
+
93
+ if (result.status === 0) {
94
+ console.log(chalk.green("WAVE Code updated successfully!"));
95
+ process.exit(0);
96
+ } else {
97
+ console.log(chalk.red("Failed to update WAVE Code."));
98
+ console.log(
99
+ chalk.yellow(
100
+ `Please try running the update command manually: ${updateCmd} ${args.join(" ")}`,
101
+ ),
102
+ );
103
+ if (process.platform !== "win32") {
104
+ console.log(
105
+ chalk.yellow(
106
+ "You might need to run it with sudo if you encounter permission issues.",
107
+ ),
108
+ );
109
+ }
110
+ process.exit(1);
111
+ }
112
+ } catch (error) {
113
+ console.error(chalk.red("Error checking for updates:"), error);
114
+ process.exit(1);
115
+ }
116
+ }
@@ -133,7 +133,7 @@ const ChatInterfaceWithRemount: React.FC = () => {
133
133
 
134
134
  if (newKey !== remountKey) {
135
135
  const timeout = setTimeout(() => {
136
- stdout?.write("\u001b[2J\u001b[0;0H", (err?: Error | null) => {
136
+ stdout?.write("\u001b[2J\u001b[3J\u001b[0;0H", (err?: Error | null) => {
137
137
  if (err) {
138
138
  console.error("Failed to clear terminal:", err);
139
139
  }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { Markdown } from "./Markdown.js";
4
+ import { BtwState } from "../managers/inputReducer.js";
5
+
6
+ interface BtwDisplayProps {
7
+ btwState: BtwState;
8
+ }
9
+
10
+ export const BtwDisplay: React.FC<BtwDisplayProps> = ({ btwState }) => {
11
+ if (!btwState.isActive) {
12
+ return null;
13
+ }
14
+
15
+ return (
16
+ <Box flexDirection="column" marginTop={0} marginBottom={1}>
17
+ {btwState.question && (
18
+ <Box>
19
+ <Text color={btwState.isLoading ? "yellow" : "green"}>● </Text>
20
+ <Text italic color="gray">
21
+ /btw {btwState.question}
22
+ </Text>
23
+ </Box>
24
+ )}
25
+
26
+ {btwState.answer && (
27
+ <Box flexDirection="column">
28
+ <Markdown>{btwState.answer}</Markdown>
29
+ </Box>
30
+ )}
31
+ </Box>
32
+ );
33
+ };
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useCallback, useLayoutEffect } from "react";
2
2
  import { Box, useStdout } from "ink";
3
3
  import { MessageList } from "./MessageList.js";
4
+ import { BtwDisplay } from "./BtwDisplay.js";
4
5
  import { InputBox } from "./InputBox.js";
5
6
  import { LoadingIndicator } from "./LoadingIndicator.js";
6
7
  import { TaskList } from "./TaskList.js";
@@ -40,11 +41,9 @@ export const ChatInterface: React.FC = () => {
40
41
  setWasLastDetailsTooTall,
41
42
  version,
42
43
  workdir,
43
- getModelConfig,
44
+ btwState,
44
45
  } = useChat();
45
46
 
46
- const model = getModelConfig().model;
47
-
48
47
  const displayMessages = messages;
49
48
 
50
49
  const handleDetailsHeightMeasured = useCallback((height: number) => {
@@ -123,21 +122,35 @@ export const ChatInterface: React.FC = () => {
123
122
  forceStatic={isConfirmationVisible && isConfirmationTooTall}
124
123
  version={version}
125
124
  workdir={workdir}
126
- model={model}
127
125
  onDynamicBlocksHeightMeasured={handleDynamicBlocksHeightMeasured}
128
126
  />
129
127
 
130
- {(isLoading || isCommandRunning || isCompressing) &&
131
- !isConfirmationVisible &&
132
- !isExpanded && (
133
- <LoadingIndicator
128
+ {!isConfirmationVisible && !isExpanded && (
129
+ <>
130
+ <BtwDisplay btwState={btwState} />
131
+ {(isLoading || isCommandRunning || isCompressing) && (
132
+ <LoadingIndicator
133
+ isLoading={isLoading}
134
+ isCommandRunning={isCommandRunning}
135
+ isCompressing={isCompressing}
136
+ latestTotalTokens={latestTotalTokens}
137
+ />
138
+ )}
139
+ <TaskList />
140
+ <QueuedMessageList />
141
+ <InputBox
134
142
  isLoading={isLoading}
135
143
  isCommandRunning={isCommandRunning}
136
- isCompressing={isCompressing}
137
- latestTotalTokens={latestTotalTokens}
144
+ sendMessage={sendMessage}
145
+ abortMessage={abortMessage}
146
+ mcpServers={mcpServers}
147
+ connectMcpServer={connectMcpServer}
148
+ disconnectMcpServer={disconnectMcpServer}
149
+ slashCommands={slashCommands}
150
+ hasSlashCommand={hasSlashCommand}
138
151
  />
139
- )}
140
- {!isConfirmationVisible && !isExpanded && <TaskList />}
152
+ </>
153
+ )}
141
154
 
142
155
  {isConfirmationVisible && (
143
156
  <>
@@ -162,23 +175,6 @@ export const ChatInterface: React.FC = () => {
162
175
  />
163
176
  </>
164
177
  )}
165
-
166
- {!isConfirmationVisible && !isExpanded && (
167
- <>
168
- <QueuedMessageList />
169
- <InputBox
170
- isLoading={isLoading}
171
- isCommandRunning={isCommandRunning}
172
- sendMessage={sendMessage}
173
- abortMessage={abortMessage}
174
- mcpServers={mcpServers}
175
- connectMcpServer={connectMcpServer}
176
- disconnectMcpServer={disconnectMcpServer}
177
- slashCommands={slashCommands}
178
- hasSlashCommand={hasSlashCommand}
179
- />
180
- </>
181
- )}
182
178
  </Box>
183
179
  );
184
180
  };
@@ -9,6 +9,7 @@ import {
9
9
  } from "wave-agent-sdk";
10
10
  import { DiffDisplay } from "./DiffDisplay.js";
11
11
  import { PlanDisplay } from "./PlanDisplay.js";
12
+ import { highlightToAnsi } from "../utils/highlightUtils.js";
12
13
 
13
14
  // Helper function to generate descriptive action text
14
15
  const getActionDescription = (
@@ -87,6 +88,19 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
87
88
  startLineNumber={startLineNumber}
88
89
  />
89
90
 
91
+ {toolName !== WRITE_TOOL_NAME &&
92
+ toolName !== EDIT_TOOL_NAME &&
93
+ toolName !== EXIT_PLAN_MODE_TOOL_NAME &&
94
+ toolName !== ASK_USER_QUESTION_TOOL_NAME &&
95
+ toolName !== BASH_TOOL_NAME &&
96
+ !!toolInput && (
97
+ <Box paddingLeft={2} borderLeft borderColor="cyan">
98
+ <Text>
99
+ {highlightToAnsi(JSON.stringify(toolInput, null, 2), "json")}
100
+ </Text>
101
+ </Box>
102
+ )}
103
+
90
104
  {toolName !== ASK_USER_QUESTION_TOOL_NAME &&
91
105
  toolName === EXIT_PLAN_MODE_TOOL_NAME &&
92
106
  !!planContent && (
@@ -10,6 +10,8 @@ import { RewindCommand } from "./RewindCommand.js";
10
10
  import { HelpView } from "./HelpView.js";
11
11
  import { StatusCommand } from "./StatusCommand.js";
12
12
  import { PluginManagerShell } from "./PluginManagerShell.js";
13
+ import { ModelSelector } from "./ModelSelector.js";
14
+ import { StatusLine } from "./StatusLine.js";
13
15
  import { useInputManager } from "../hooks/useInputManager.js";
14
16
  import { useChat } from "../contexts/useChat.js";
15
17
 
@@ -54,12 +56,19 @@ export const InputBox: React.FC<InputBoxProps> = ({
54
56
  const {
55
57
  permissionMode: chatPermissionMode,
56
58
  setPermissionMode: setChatPermissionMode,
59
+ allowBypassInCycle: chatAllowBypassInCycle,
57
60
  handleRewindSelect,
58
61
  backgroundCurrentTask,
59
62
  messages,
60
63
  getFullMessageThread,
61
64
  sessionId,
62
65
  workingDirectory,
66
+ askBtw,
67
+ btwState: chatBtwState,
68
+ setBtwState: setChatBtwState,
69
+ currentModel,
70
+ configuredModels,
71
+ setModel,
63
72
  } = useChat();
64
73
 
65
74
  // Input manager with all input state and functionality (including images)
@@ -94,21 +103,29 @@ export const InputBox: React.FC<InputBoxProps> = ({
94
103
  showHelp,
95
104
  showStatusCommand,
96
105
  showPluginManager,
106
+ showModelSelector,
97
107
  setShowBackgroundTaskManager,
98
108
  setShowMcpManager,
99
109
  setShowRewindManager,
100
110
  setShowHelp,
101
111
  setShowStatusCommand,
102
112
  setShowPluginManager,
113
+ setShowModelSelector,
103
114
  // Permission mode
104
115
  permissionMode,
105
116
  setPermissionMode,
117
+ setAllowBypassInCycle,
118
+ // BTW state
119
+ btwState,
106
120
  // Main handler
107
121
  handleInput,
108
122
  // Manager ready state
109
123
  isManagerReady,
110
124
  } = useInputManager({
111
125
  onSendMessage: sendMessage,
126
+ onAskBtw: askBtw,
127
+ btwState: chatBtwState,
128
+ onBtwStateChange: setChatBtwState,
112
129
  onHasSlashCommand: hasSlashCommand,
113
130
  onAbortMessage: abortMessage,
114
131
  onBackgroundCurrentTask: backgroundCurrentTask,
@@ -123,6 +140,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
123
140
  setPermissionMode(chatPermissionMode);
124
141
  }, [chatPermissionMode, setPermissionMode]);
125
142
 
143
+ // Sync allowBypassInCycle from useChat to InputManager
144
+ useEffect(() => {
145
+ setAllowBypassInCycle(chatAllowBypassInCycle);
146
+ }, [chatAllowBypassInCycle, setAllowBypassInCycle]);
147
+
126
148
  // Use the InputManager's unified input handler
127
149
  useInput(async (input, key) => {
128
150
  await handleInput(input, key, attachedImages, clearImages);
@@ -189,6 +211,17 @@ export const InputBox: React.FC<InputBoxProps> = ({
189
211
  return <PluginManagerShell onCancel={() => setShowPluginManager(false)} />;
190
212
  }
191
213
 
214
+ if (showModelSelector) {
215
+ return (
216
+ <ModelSelector
217
+ onCancel={() => setShowModelSelector(false)}
218
+ currentModel={currentModel}
219
+ configuredModels={configuredModels}
220
+ onSelectModel={setModel}
221
+ />
222
+ );
223
+ }
224
+
192
225
  return (
193
226
  <Box flexDirection="column">
194
227
  {showFileSelector && (
@@ -243,12 +276,19 @@ export const InputBox: React.FC<InputBoxProps> = ({
243
276
  <Box flexDirection="column">
244
277
  <Box
245
278
  borderStyle="single"
246
- borderColor="gray"
279
+ borderColor={btwState.isActive ? "cyan" : "gray"}
247
280
  borderLeft={false}
248
281
  borderRight={false}
249
282
  >
250
283
  <Text color={isPlaceholder ? "gray" : "white"}>
251
- {shouldShowCursor ? (
284
+ {btwState.isActive && isPlaceholder ? (
285
+ <>
286
+ <Text backgroundColor="white" color="black">
287
+ {" "}
288
+ </Text>
289
+ <Text color="cyan">Type your side question...</Text>
290
+ </>
291
+ ) : shouldShowCursor ? (
252
292
  <>
253
293
  {beforeCursor}
254
294
  <Text backgroundColor="white" color="black">
@@ -261,21 +301,11 @@ export const InputBox: React.FC<InputBoxProps> = ({
261
301
  )}
262
302
  </Text>
263
303
  </Box>
264
- <Box paddingRight={1} justifyContent="space-between" width="100%">
265
- {isShellCommand ? (
266
- <Text color="gray">
267
- Shell: <Text color="yellow">Run shell command</Text>
268
- </Text>
269
- ) : (
270
- <Text color="gray">
271
- Mode:{" "}
272
- <Text color={permissionMode === "plan" ? "yellow" : "cyan"}>
273
- {permissionMode}
274
- </Text>{" "}
275
- (Shift+Tab to cycle)
276
- </Text>
277
- )}
278
- </Box>
304
+ <StatusLine
305
+ permissionMode={permissionMode}
306
+ isShellCommand={isShellCommand}
307
+ isBtwActive={btwState.isActive}
308
+ />
279
309
  </Box>
280
310
  )}
281
311
  </Box>
@@ -3,6 +3,7 @@ import { Box, Text } from "ink";
3
3
  import type { Message, MessageBlock } from "wave-agent-sdk";
4
4
  import { MessageSource } from "wave-agent-sdk";
5
5
  import { BangDisplay } from "./BangDisplay.js";
6
+ import { SlashDisplay } from "./SlashDisplay.js";
6
7
  import { ToolDisplay } from "./ToolDisplay.js";
7
8
  import { CompressDisplay } from "./CompressDisplay.js";
8
9
  import { ReasoningDisplay } from "./ReasoningDisplay.js";
@@ -25,11 +26,6 @@ export const MessageBlockItem = ({
25
26
  <Box flexDirection="column" paddingTop={paddingTop}>
26
27
  {block.type === "text" && block.content.trim() && (
27
28
  <Box>
28
- {block.customCommandContent && (
29
- <Text color="cyan" bold>
30
- ${" "}
31
- </Text>
32
- )}
33
29
  {block.source === MessageSource.HOOK && (
34
30
  <Text color="magenta" bold>
35
31
  ~{" "}
@@ -48,6 +44,10 @@ export const MessageBlockItem = ({
48
44
  </Box>
49
45
  )}
50
46
 
47
+ {block.type === "slash" && (
48
+ <SlashDisplay block={block} isExpanded={isExpanded} />
49
+ )}
50
+
51
51
  {block.type === "error" && (
52
52
  <Box>
53
53
  <Text color="red">Error: {block.content}</Text>
@@ -10,7 +10,6 @@ export interface MessageListProps {
10
10
  forceStatic?: boolean;
11
11
  version?: string;
12
12
  workdir?: string;
13
- model?: string;
14
13
  onDynamicBlocksHeightMeasured?: (height: number) => void;
15
14
  }
16
15
 
@@ -21,15 +20,11 @@ export const MessageList = React.memo(
21
20
  forceStatic = false,
22
21
  version,
23
22
  workdir,
24
- model,
25
23
  onDynamicBlocksHeightMeasured,
26
24
  }: MessageListProps) => {
27
25
  const welcomeMessage = (
28
26
  <Box flexDirection="column" paddingTop={1}>
29
- <Text color="gray">
30
- WAVE{version ? ` v${version}` : ""}
31
- {model ? ` • ${model}` : ""}
32
- </Text>
27
+ <Text color="gray">WAVE{version ? ` v${version}` : ""}</Text>
33
28
  {workdir && (
34
29
  <Text color="gray" wrap="truncate-middle">
35
30
  {workdir.replace(os.homedir(), "~")}
@@ -41,8 +36,11 @@ export const MessageList = React.memo(
41
36
  // Limit messages to prevent long rendering times
42
37
  const maxMessages = 10;
43
38
 
39
+ // Filter out meta messages
40
+ const visibleMessages = messages.filter((m) => !m.isMeta);
41
+
44
42
  // Flatten messages into blocks with metadata
45
- const allBlocks = messages.flatMap((message, messageIndex) => {
43
+ const allBlocks = visibleMessages.flatMap((message, messageIndex) => {
46
44
  return message.blocks.map((block, blockIndex) => ({
47
45
  block,
48
46
  message,
@@ -58,7 +56,8 @@ export const MessageList = React.memo(
58
56
  const isDynamic =
59
57
  !forceStatic &&
60
58
  ((block.type === "tool" && block.stage === "running") ||
61
- (block.type === "bang" && block.isRunning));
59
+ (block.type === "bang" && block.isRunning) ||
60
+ (block.type === "slash" && block.stage === "running"));
62
61
  return { ...item, isDynamic };
63
62
  });
64
63
 
@@ -101,8 +100,8 @@ export const MessageList = React.memo(
101
100
  );
102
101
  }
103
102
  if (
104
- messages.length > maxMessages &&
105
- item.messageIndex < messages.length - maxMessages
103
+ visibleMessages.length > maxMessages &&
104
+ item.messageIndex < visibleMessages.length - maxMessages
106
105
  ) {
107
106
  return null;
108
107
  }
@@ -0,0 +1,109 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ export interface ModelSelectorProps {
5
+ onCancel: () => void;
6
+ currentModel: string;
7
+ configuredModels: string[];
8
+ onSelectModel: (model: string) => void;
9
+ }
10
+
11
+ export const ModelSelector: React.FC<ModelSelectorProps> = ({
12
+ onCancel,
13
+ currentModel,
14
+ configuredModels,
15
+ onSelectModel,
16
+ }) => {
17
+ const [selectedIndex, setSelectedIndex] = useState(() => {
18
+ const index = configuredModels.indexOf(currentModel);
19
+ return index !== -1 ? index : 0;
20
+ });
21
+
22
+ useInput((_input, key) => {
23
+ if (key.return) {
24
+ if (configuredModels.length > 0) {
25
+ onSelectModel(configuredModels[selectedIndex]);
26
+ }
27
+ onCancel();
28
+ return;
29
+ }
30
+
31
+ if (key.escape) {
32
+ onCancel();
33
+ return;
34
+ }
35
+
36
+ if (key.upArrow) {
37
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
38
+ return;
39
+ }
40
+
41
+ if (key.downArrow) {
42
+ setSelectedIndex((prev) =>
43
+ Math.min(configuredModels.length - 1, prev + 1),
44
+ );
45
+ return;
46
+ }
47
+ });
48
+
49
+ if (configuredModels.length === 0) {
50
+ return (
51
+ <Box
52
+ flexDirection="column"
53
+ borderStyle="single"
54
+ borderColor="cyan"
55
+ borderBottom={false}
56
+ borderLeft={false}
57
+ borderRight={false}
58
+ paddingTop={1}
59
+ >
60
+ <Text color="cyan" bold>
61
+ Select AI Model
62
+ </Text>
63
+ <Text>No models configured</Text>
64
+ <Text dimColor>Press Escape to close</Text>
65
+ </Box>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <Box
71
+ flexDirection="column"
72
+ borderStyle="single"
73
+ borderColor="cyan"
74
+ borderBottom={false}
75
+ borderLeft={false}
76
+ borderRight={false}
77
+ paddingTop={1}
78
+ gap={1}
79
+ >
80
+ <Box>
81
+ <Text color="cyan" bold>
82
+ Select AI Model
83
+ </Text>
84
+ </Box>
85
+ <Text dimColor>Select a model to use for the current session</Text>
86
+
87
+ {configuredModels.map((model, index) => (
88
+ <Box key={model}>
89
+ <Text
90
+ color={index === selectedIndex ? "black" : "white"}
91
+ backgroundColor={index === selectedIndex ? "cyan" : undefined}
92
+ >
93
+ {index === selectedIndex ? "▶ " : " "}
94
+ {model}
95
+ {model === currentModel ? (
96
+ <Text color="green"> (current)</Text>
97
+ ) : (
98
+ ""
99
+ )}
100
+ </Text>
101
+ </Box>
102
+ ))}
103
+
104
+ <Box marginTop={1}>
105
+ <Text dimColor>↑/↓ to select · Enter to confirm · Esc to cancel</Text>
106
+ </Box>
107
+ </Box>
108
+ );
109
+ };
@@ -30,10 +30,10 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
30
30
  }
31
31
  }, [getFullMessageThread]);
32
32
 
33
- // Filter user messages as checkpoints
33
+ // Filter user messages as checkpoints, excluding meta messages
34
34
  const checkpoints = messages
35
35
  .map((msg, index) => ({ msg, index }))
36
- .filter(({ msg }) => msg.role === "user");
36
+ .filter(({ msg }) => msg.role === "user" && !msg.isMeta);
37
37
 
38
38
  const MAX_VISIBLE_ITEMS = 3;
39
39
  const [selectedIndex, setSelectedIndex] = useState(checkpoints.length - 1);