wave-code 0.19.9 → 1.0.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 (83) hide show
  1. package/dist/cli.js +20 -1
  2. package/dist/components/App.js +7 -0
  3. package/dist/components/BtwDisplay.js +13 -3
  4. package/dist/components/ChatInterface.js +21 -6
  5. package/dist/components/HelpView.js +6 -0
  6. package/dist/components/InputBox.d.ts +1 -2
  7. package/dist/components/InputBox.js +16 -5
  8. package/dist/components/LoadingIndicator.d.ts +1 -2
  9. package/dist/components/LoadingIndicator.js +2 -2
  10. package/dist/components/Markdown.js +13 -16
  11. package/dist/components/MessageList.d.ts +2 -1
  12. package/dist/components/MessageList.js +2 -2
  13. package/dist/components/RewindCommand.js +4 -2
  14. package/dist/components/StatusLine.d.ts +0 -2
  15. package/dist/components/StatusLine.js +6 -6
  16. package/dist/components/TaskList.js +2 -1
  17. package/dist/components/ToolDisplay.d.ts +1 -0
  18. package/dist/components/ToolDisplay.js +17 -9
  19. package/dist/constants/commands.js +0 -6
  20. package/dist/contexts/useChat.d.ts +3 -5
  21. package/dist/contexts/useChat.js +242 -82
  22. package/dist/daemon-cli.d.ts +10 -0
  23. package/dist/daemon-cli.js +15 -0
  24. package/dist/hooks/useInputManager.d.ts +1 -0
  25. package/dist/hooks/useInputManager.js +120 -40
  26. package/dist/index.js +10 -0
  27. package/dist/managers/inputHandlers.js +55 -30
  28. package/dist/managers/inputReducer.d.ts +22 -22
  29. package/dist/managers/inputReducer.js +361 -177
  30. package/dist/print-cli.js +36 -10
  31. package/dist/stdio/agentBridge.d.ts +23 -0
  32. package/dist/stdio/agentBridge.js +151 -18
  33. package/dist/stdio/daemonServer.d.ts +67 -0
  34. package/dist/stdio/daemonServer.js +191 -0
  35. package/dist/stdio/index.d.ts +2 -0
  36. package/dist/stdio/index.js +2 -0
  37. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  38. package/dist/stdio/jsonRpcConnection.js +127 -0
  39. package/dist/stdio/protocol.d.ts +2 -2
  40. package/dist/stdio/stdioServer.d.ts +2 -7
  41. package/dist/stdio/stdioServer.js +9 -100
  42. package/dist/utils/bracketedPaste.d.ts +39 -0
  43. package/dist/utils/bracketedPaste.js +122 -0
  44. package/dist/utils/markdownTable.d.ts +34 -0
  45. package/dist/utils/markdownTable.js +302 -0
  46. package/dist/utils/rewindCheckpoints.d.ts +8 -0
  47. package/dist/utils/rewindCheckpoints.js +15 -0
  48. package/dist/utils/throttle.d.ts +3 -3
  49. package/dist/utils/worktree.d.ts +8 -0
  50. package/dist/utils/worktree.js +32 -1
  51. package/package.json +4 -2
  52. package/src/cli.tsx +20 -1
  53. package/src/components/App.tsx +5 -0
  54. package/src/components/BtwDisplay.tsx +36 -12
  55. package/src/components/ChatInterface.tsx +26 -11
  56. package/src/components/HelpView.tsx +6 -0
  57. package/src/components/InputBox.tsx +21 -10
  58. package/src/components/LoadingIndicator.tsx +1 -4
  59. package/src/components/Markdown.tsx +15 -18
  60. package/src/components/MessageList.tsx +6 -0
  61. package/src/components/RewindCommand.tsx +4 -2
  62. package/src/components/StatusLine.tsx +0 -10
  63. package/src/components/TaskList.tsx +2 -1
  64. package/src/components/ToolDisplay.tsx +17 -6
  65. package/src/constants/commands.ts +0 -6
  66. package/src/contexts/useChat.tsx +310 -95
  67. package/src/daemon-cli.ts +17 -0
  68. package/src/hooks/useInputManager.ts +135 -43
  69. package/src/index.ts +12 -0
  70. package/src/managers/inputHandlers.ts +55 -32
  71. package/src/managers/inputReducer.ts +442 -214
  72. package/src/print-cli.ts +48 -11
  73. package/src/stdio/agentBridge.ts +213 -18
  74. package/src/stdio/daemonServer.ts +212 -0
  75. package/src/stdio/index.ts +2 -0
  76. package/src/stdio/jsonRpcConnection.ts +160 -0
  77. package/src/stdio/protocol.ts +5 -2
  78. package/src/stdio/stdioServer.ts +14 -120
  79. package/src/utils/bracketedPaste.ts +170 -0
  80. package/src/utils/markdownTable.ts +359 -0
  81. package/src/utils/rewindCheckpoints.ts +15 -0
  82. package/src/utils/throttle.ts +8 -8
  83. package/src/utils/worktree.ts +50 -1
package/dist/cli.js CHANGED
@@ -7,6 +7,16 @@ export async function startCli(options) {
7
7
  const { restoreSessionId, continueLastSession, bypassPermissions, permissionMode, pluginDirs, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, } = options;
8
8
  // Continue with ink-based UI for normal mode
9
9
  let shouldRemoveWorktree = false;
10
+ // Enable bracketed paste (DECSET 2004) so terminals wrap pasted text in
11
+ // \x1b[200~ ... \x1b[201~ markers. The input pipeline uses these to insert
12
+ // pasted text without submitting (a pasted trailing \r is not an Enter).
13
+ // Terminals without bracketed-paste support ignore the sequence and paste
14
+ // without markers, falling back to legacy behavior. No-op when stdout is
15
+ // not a TTY (piped input, stdio mode).
16
+ const stdoutIsTTY = process.stdout.isTTY === true;
17
+ if (stdoutIsTTY) {
18
+ process.stdout.write("\x1b[?2004h");
19
+ }
10
20
  const handleExit = (shouldRemove) => {
11
21
  shouldRemoveWorktree = shouldRemove;
12
22
  unmount();
@@ -14,7 +24,16 @@ export async function startCli(options) {
14
24
  // Render the application
15
25
  const { unmount, waitUntilExit } = render(_jsx(App, { restoreSessionId: restoreSessionId, continueLastSession: continueLastSession, bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, worktreeSession: worktreeSession, workdir: workdir, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, onExit: handleExit }), { exitOnCtrlC: false });
16
26
  // Wait for the app to finish unmounting
17
- await waitUntilExit();
27
+ try {
28
+ await waitUntilExit();
29
+ }
30
+ finally {
31
+ // Disable bracketed paste (DECSET 2004) so the terminal returns to its
32
+ // previous paste handling.
33
+ if (stdoutIsTTY) {
34
+ process.stdout.write("\x1b[?2004l");
35
+ }
36
+ }
18
37
  try {
19
38
  // Clean up old log files
20
39
  await cleanupLogs().catch((error) => {
@@ -6,6 +6,7 @@ import { ChatProvider, useChat } from "../contexts/useChat.js";
6
6
  import { AppProvider } from "../contexts/useAppConfig.js";
7
7
  import { WorktreeExitPrompt } from "./WorktreeExitPrompt.js";
8
8
  import { hasUncommittedChanges, hasNewCommits, getDefaultRemoteBranch, } from "wave-agent-sdk";
9
+ import { btwOverlayActiveRef } from "../managers/inputReducer.js";
9
10
  /** Wraps ChatInterface with worktree exit handling, using useChat() for hook access. */
10
11
  const ChatWithExitPrompt = ({ worktreeSession, onExit }) => {
11
12
  const { triggerWorktreeRemoveHook } = useChat();
@@ -28,6 +29,9 @@ const ChatWithExitPrompt = ({ worktreeSession, onExit }) => {
28
29
  }
29
30
  }, [worktreeSession, onExit]);
30
31
  useInput((input, key) => {
32
+ // While the /btw overlay is up it owns the keys; Ctrl+C must not quit
33
+ if (btwOverlayActiveRef.current)
34
+ return;
31
35
  if (input === "c" && key.ctrl) {
32
36
  handleSignal();
33
37
  }
@@ -54,6 +58,9 @@ const AppWithProviders = ({ bypassPermissions, permissionMode, pluginDirs, tools
54
58
  // Handle Ctrl-C for non-worktree sessions (immediate exit)
55
59
  // Ink runs terminal in raw mode, so Ctrl+C arrives as useInput event, not SIGINT
56
60
  useInput((input, key) => {
61
+ // While the /btw overlay is up it owns the keys; Ctrl+C must not quit
62
+ if (btwOverlayActiveRef.current)
63
+ return;
57
64
  if (!worktreeSession && input === "c" && key.ctrl) {
58
65
  onExit(false);
59
66
  return true;
@@ -1,9 +1,19 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { Markdown } from "./Markdown.js";
4
4
  export const BtwDisplay = ({ btwState }) => {
5
- if (!btwState.question) {
5
+ // Rendered for a real question (loading or answered) and for the bare
6
+ // `/btw` usage message (question === "", answer set).
7
+ if (!btwState.question && !btwState.answer) {
6
8
  return null;
7
9
  }
8
- return (_jsxs(Box, { flexDirection: "column", marginTop: 0, marginBottom: 0, children: [btwState.question && (_jsxs(Box, { children: [_jsx(Text, { color: btwState.isLoading ? "yellow" : "green", children: "/ " }), _jsxs(Text, { italic: true, color: "gray", children: ["btw ", btwState.question] })] })), btwState.answer && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Markdown, { children: btwState.answer }), _jsx(Text, { color: "gray", dimColor: true, children: "ESC to dismiss" })] }))] }));
10
+ const answer = btwState.answer ?? "";
11
+ // The SDK surfaces failure as the answer string; classify by prefix so it
12
+ // renders in error color (aligned with Claude Code's error display).
13
+ const isError = !btwState.isLoading &&
14
+ (answer.startsWith("(API error") ||
15
+ answer.startsWith("(The model tried to call") ||
16
+ answer === "No response received");
17
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [btwState.question && (_jsxs(Box, { children: [_jsxs(Text, { color: "warning", bold: true, children: ["/btw", " "] }), _jsx(Text, { dimColor: true, children: btwState.question })] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: btwState.isLoading ? (_jsx(Text, { color: "gray", children: "\u273B Answering..." })) : isError ? (_jsx(Text, { color: "error", children: answer })) : (_jsx(Markdown, { children: answer })) }), btwState.question && btwState.answer && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Escape to dismiss" }) })), !btwState.question && btwState.answer && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Escape to dismiss" }) }))] }));
9
18
  };
19
+ BtwDisplay.displayName = "BtwDisplay";
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useRef, useEffect } from "react";
2
+ import { useState, useRef, useEffect, useCallback } from "react";
3
3
  import { Box, useStdout, measureElement, Static } from "ink";
4
+ import { authService } from "wave-agent-sdk";
4
5
  import { MessageList } from "./MessageList.js";
5
6
  import { InputBox } from "./InputBox.js";
6
7
  import { LoadingIndicator } from "./LoadingIndicator.js";
@@ -10,12 +11,29 @@ import { ConfirmationDetails } from "./ConfirmationDetails.js";
10
11
  import { ConfirmationSelector } from "./ConfirmationSelector.js";
11
12
  import { useChat } from "../contexts/useChat.js";
12
13
  export const ChatInterface = () => {
13
- const { messages, isLoading, isCommandRunning, isCompacting, sendMessage, abortMessage, mcpServers, connectMcpServer, disconnectMcpServer, isExpanded, sessionId, latestTotalTokens, maxInputTokens, slashCommands, hasSlashCommand, isConfirmationVisible, hasPendingConfirmations, confirmingTool, handleConfirmationDecision, handleConfirmationCancel, version, workdir, remountKey, requestRemount, isGoalActive, goalElapsed, isGoalEvaluating, } = useChat();
14
+ const { messages, isLoading, isCommandRunning, isCompacting, sendMessage, abortMessage, mcpServers, connectMcpServer, disconnectMcpServer, isExpanded, sessionId, latestTotalTokens, maxInputTokens, slashCommands, hasSlashCommand, isConfirmationVisible, hasPendingConfirmations, confirmingTool, handleConfirmationDecision, handleConfirmationCancel, version, workdir, remountKey, requestRemount, getGatewayConfig, } = useChat();
14
15
  const displayMessages = messages;
15
16
  const [forceStatic, setForceStatic] = useState(false);
16
17
  const { stdout } = useStdout();
17
18
  const terminalHeight = stdout?.rows ?? 24;
18
19
  const chatInterfaceRef = useRef(null);
20
+ // Compute whether the user has any usable auth/direct-API config,
21
+ // so the welcome page can prompt /login when neither is present.
22
+ const computeAuthState = useCallback(() => {
23
+ if (authService.isSSOAuthenticated())
24
+ return true;
25
+ const gateway = getGatewayConfig();
26
+ return Boolean(gateway.apiKey || gateway.baseURL);
27
+ }, [getGatewayConfig]);
28
+ const [hasAuth, setHasAuth] = useState(computeAuthState);
29
+ // Keep the /login hint in sync with auth state changes (login/logout).
30
+ useEffect(() => {
31
+ const unsubscribe = authService.onAuthChange(() => {
32
+ setHasAuth(computeAuthState());
33
+ });
34
+ return unsubscribe;
35
+ }, [computeAuthState]);
36
+ const showLoginHint = !hasAuth;
19
37
  // Handle forceStatic mode for overflow and request remount when exiting
20
38
  useEffect(() => {
21
39
  if (isConfirmationVisible && chatInterfaceRef.current) {
@@ -37,8 +55,5 @@ export const ChatInterface = () => {
37
55
  ]);
38
56
  if (!sessionId)
39
57
  return null;
40
- return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading ||
41
- isCommandRunning ||
42
- isCompacting ||
43
- isGoalEvaluating) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, isGoalEvaluating: isGoalEvaluating, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, isGoalActive: isGoalActive, goalElapsed: goalElapsed })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
58
+ return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic, showLoginHint: showLoginHint }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompacting) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
44
59
  };
@@ -47,11 +47,17 @@ export const HelpView = ({ onCancel, commands = [], }) => {
47
47
  { key: "Ctrl+B", description: "Background current task" },
48
48
  { key: "Ctrl+V", description: "Paste image" },
49
49
  { key: "Ctrl+J", description: "Newline" },
50
+ { key: "Ctrl+A", description: "Cursor to line start" },
51
+ { key: "Ctrl+E", description: "Cursor to line end" },
52
+ { key: "Ctrl+U", description: "Delete to line start" },
53
+ { key: "Ctrl+K", description: "Delete to line end" },
54
+ { key: "Ctrl+W", description: "Delete word before cursor" },
50
55
  { key: "Shift+Tab", description: "Cycle permission mode" },
51
56
  {
52
57
  key: "Esc",
53
58
  description: "Interrupt AI or command / Cancel selector / Close help",
54
59
  },
60
+ { key: "Esc ×2", description: "Clear input (idle)" },
55
61
  ];
56
62
  const footerText = [
57
63
  "Tab switch",
@@ -5,6 +5,7 @@ export declare const INPUT_PLACEHOLDER_TEXT_PREFIX: string;
5
5
  export interface InputBoxProps {
6
6
  isLoading?: boolean;
7
7
  isCommandRunning?: boolean;
8
+ isCompacting?: boolean;
8
9
  workdir?: string;
9
10
  sendMessage?: (message: string, images?: Array<{
10
11
  path: string;
@@ -18,7 +19,5 @@ export interface InputBoxProps {
18
19
  hasSlashCommand?: (commandId: string) => boolean;
19
20
  latestTotalTokens?: number;
20
21
  maxInputTokens?: number;
21
- isGoalActive?: boolean;
22
- goalElapsed?: string;
23
22
  }
24
23
  export declare const InputBox: React.FC<InputBoxProps>;
@@ -20,11 +20,14 @@ import { useInputManager } from "../hooks/useInputManager.js";
20
20
  import { useChat } from "../contexts/useChat.js";
21
21
  export const INPUT_PLACEHOLDER_TEXT = "Type your message (use /help for more info)...";
22
22
  export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(0, 10);
23
- export const InputBox = ({ sendMessage = () => { }, abortMessage = () => { }, mcpServers = [], connectMcpServer = async () => false, disconnectMcpServer = async () => false, slashCommands = [], hasSlashCommand = () => false, latestTotalTokens = 0, maxInputTokens = 200000, isGoalActive, goalElapsed, }) => {
24
- const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact, goalCommand, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, } = useChat();
23
+ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessage = () => { }, abortMessage = () => { }, mcpServers = [], connectMcpServer = async () => false, disconnectMcpServer = async () => false, slashCommands = [], hasSlashCommand = () => false, latestTotalTokens = 0, maxInputTokens = 200000, }) => {
24
+ const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, setIsBtwActive, } = useChat();
25
25
  // Ref to hold setInputText so queue callbacks can access it before useInputManager returns
26
26
  const setInputTextRef = useRef(() => { });
27
27
  const hasQueuedMessages = (queuedMessages?.length ?? 0) > 0;
28
+ // Idle means no AI work in flight. Esc double-press clear only applies when
29
+ // idle; while busy, Esc keeps its abort semantics.
30
+ const isIdle = !(isLoading || isCommandRunning || isCompacting);
28
31
  const onRecallQueuedMessage = useCallback(() => {
29
32
  const msg = recallQueuedMessage();
30
33
  if (msg) {
@@ -46,6 +49,8 @@ export const InputBox = ({ sendMessage = () => { }, abortMessage = () => { }, mc
46
49
  permissionMode, setPermissionMode,
47
50
  // BTW state
48
51
  btwState,
52
+ // Esc double-press clear pending
53
+ escClearPending,
49
54
  // Main handler
50
55
  handleInput,
51
56
  // Manager ready state
@@ -56,7 +61,6 @@ export const InputBox = ({ sendMessage = () => { }, abortMessage = () => { }, mc
56
61
  onAskBtw: askBtw,
57
62
  onClearMessages: clearMessages,
58
63
  onCompact: compact,
59
- onGoalCommand: goalCommand,
60
64
  onHasSlashCommand: hasSlashCommand,
61
65
  onAbortMessage: abortMessage,
62
66
  onBackgroundCurrentTask: backgroundCurrentTask,
@@ -65,12 +69,19 @@ export const InputBox = ({ sendMessage = () => { }, abortMessage = () => { }, mc
65
69
  workdir: workingDirectory,
66
70
  getFullMessageThread,
67
71
  hasQueuedMessages,
72
+ isIdle,
68
73
  onRecallQueuedMessage,
69
74
  });
70
75
  // Keep setInputText ref updated for queue callbacks
71
76
  useEffect(() => {
72
77
  setInputTextRef.current = setInputText;
73
78
  }, [setInputText]);
79
+ // Sync the btw overlay's visibility to ChatContext so siblings (TaskList)
80
+ // can hide while the side-question is on display (aligned with Claude Code,
81
+ // which suppresses the expanded task list while a local-jsx command shows).
82
+ useEffect(() => {
83
+ setIsBtwActive(btwState.question !== "" || btwState.answer !== undefined);
84
+ }, [btwState.question, btwState.answer, setIsBtwActive]);
74
85
  // Sync permission mode from useChat to InputManager
75
86
  useEffect(() => {
76
87
  setPermissionMode(chatPermissionMode);
@@ -137,7 +148,7 @@ export const InputBox = ({ sendMessage = () => { }, abortMessage = () => { }, mc
137
148
  if (showModelSelector) {
138
149
  return (_jsx(ModelSelector, { onCancel: () => setShowModelSelector(false), currentModel: currentModel, configuredModels: configuredModels, onSelectModel: setModel }));
139
150
  }
140
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(BtwDisplay, { btwState: btwState }), showFileSelector && (_jsx(FileSelector, { files: filteredFiles, searchQuery: searchQuery, isLoading: isFileSearching, onSelect: handleFileSelect, onCancel: handleCancelFileSelect })), showCommandSelector && (_jsx(CommandSelector, { searchQuery: commandSearchQuery, onSelect: handleCommandSelect, onInsert: handleCommandInsert, onCancel: handleCancelCommandSelect, commands: slashCommands })), showHistorySearch && (_jsx(HistorySearch, { searchQuery: historySearchQuery, onSelect: handleHistorySearchSelect, onCancel: handleCancelHistorySearch })), showBackgroundTaskManager && (_jsx(BackgroundTaskManager, { onCancel: () => setShowBackgroundTaskManager(false) })), showMcpManager && (_jsx(McpManager, { onCancel: () => setShowMcpManager(false), servers: mcpServers, onConnectServer: connectMcpServer, onDisconnectServer: disconnectMcpServer })), showWorkflowManager && (_jsx(WorkflowManager, { onCancel: () => setShowWorkflowManager(false) })), btwState.question
151
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(BtwDisplay, { btwState: btwState }), showFileSelector && (_jsx(FileSelector, { files: filteredFiles, searchQuery: searchQuery, isLoading: isFileSearching, onSelect: handleFileSelect, onCancel: handleCancelFileSelect })), showCommandSelector && (_jsx(CommandSelector, { searchQuery: commandSearchQuery, onSelect: handleCommandSelect, onInsert: handleCommandInsert, onCancel: handleCancelCommandSelect, commands: slashCommands })), showHistorySearch && (_jsx(HistorySearch, { searchQuery: historySearchQuery, onSelect: handleHistorySearchSelect, onCancel: handleCancelHistorySearch })), showBackgroundTaskManager && (_jsx(BackgroundTaskManager, { onCancel: () => setShowBackgroundTaskManager(false) })), showMcpManager && (_jsx(McpManager, { onCancel: () => setShowMcpManager(false), servers: mcpServers, onConnectServer: connectMcpServer, onDisconnectServer: disconnectMcpServer })), showWorkflowManager && (_jsx(WorkflowManager, { onCancel: () => setShowWorkflowManager(false) })), btwState.question || btwState.answer
141
152
  ? null
142
153
  : showBackgroundTaskManager ||
143
154
  showMcpManager ||
@@ -146,5 +157,5 @@ export const InputBox = ({ sendMessage = () => { }, abortMessage = () => { }, mc
146
157
  showStatusCommand ||
147
158
  showLoginCommand ||
148
159
  showPluginManager ||
149
- showWorkflowManager || (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: _jsx(Text, { color: isPlaceholder ? "gray" : "white", children: shouldShowCursor ? (_jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { backgroundColor: "white", color: "black", children: atCursor }), afterCursor] })) : (displayText) }) }), _jsx(StatusLine, { permissionMode: permissionMode, isShellCommand: isShellCommand, isGoalActive: isGoalActive, goalElapsed: goalElapsed, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens })] }))] }));
160
+ showWorkflowManager || (_jsxs(Box, { flexDirection: "column", children: [escClearPending && _jsx(Text, { color: "gray", children: "\u518D\u6B21\u6309 Esc \u6E05\u7A7A\u8F93\u5165" }), _jsx(Box, { borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: _jsx(Text, { color: isPlaceholder ? "gray" : "white", children: shouldShowCursor ? (_jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { backgroundColor: "white", color: "black", children: atCursor }), afterCursor] })) : (displayText) }) }), _jsx(StatusLine, { permissionMode: permissionMode, isShellCommand: isShellCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens })] }))] }));
150
161
  };
@@ -2,10 +2,9 @@ export interface LoadingIndicatorProps {
2
2
  isLoading?: boolean;
3
3
  isCommandRunning?: boolean;
4
4
  isCompacting?: boolean;
5
- isGoalEvaluating?: boolean;
6
5
  latestTotalTokens?: number;
7
6
  }
8
7
  export declare const LoadingIndicator: {
9
- ({ isLoading, isCommandRunning, isCompacting, isGoalEvaluating, latestTotalTokens, }: LoadingIndicatorProps): import("react/jsx-runtime").JSX.Element;
8
+ ({ isLoading, isCommandRunning, isCompacting, latestTotalTokens, }: LoadingIndicatorProps): import("react/jsx-runtime").JSX.Element;
10
9
  displayName: string;
11
10
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- export const LoadingIndicator = ({ isLoading = false, isCommandRunning = false, isCompacting = false, isGoalEvaluating = false, latestTotalTokens = 0, }) => {
4
- return (_jsxs(Box, { flexDirection: "column", children: [isLoading && !isCompacting && !isGoalEvaluating && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u273B AI is thinking... " }), latestTotalTokens > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: ["|", " "] }), _jsx(Text, { color: "blue", bold: true, children: latestTotalTokens.toLocaleString() }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", "tokens", " "] })] })), _jsxs(Text, { color: "gray", dimColor: true, children: ["|", " "] }), _jsx(Text, { color: "red", bold: true, children: "Esc" }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", "to abort"] })] })), isCommandRunning && _jsx(Text, { color: "blue", children: "\u273B Command is running..." }), isCompacting && (_jsx(Text, { color: "magenta", children: "\u273B Compacting message history..." })), isGoalEvaluating && _jsx(Text, { color: "cyan", children: "\u273B Evaluating goal..." })] }));
3
+ export const LoadingIndicator = ({ isLoading = false, isCommandRunning = false, isCompacting = false, latestTotalTokens = 0, }) => {
4
+ return (_jsxs(Box, { flexDirection: "column", children: [isLoading && !isCompacting && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u273B AI is thinking... " }), latestTotalTokens > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", dimColor: true, children: ["|", " "] }), _jsx(Text, { color: "blue", bold: true, children: latestTotalTokens.toLocaleString() }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", "tokens", " "] })] })), _jsxs(Text, { color: "gray", dimColor: true, children: ["|", " "] }), _jsx(Text, { color: "red", bold: true, children: "Esc" }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", "to abort"] })] })), isCommandRunning && _jsx(Text, { color: "blue", children: "\u273B Command is running..." }), isCompacting && (_jsx(Text, { color: "magenta", children: "\u273B Compacting message history..." }))] }));
5
5
  };
6
6
  LoadingIndicator.displayName = "LoadingIndicator";
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useMemo } from "react";
3
- import { Box, Text } from "ink";
3
+ import { Box, Text, useStdout } from "ink";
4
4
  import { Renderer, marked } from "marked";
5
5
  import chalk from "chalk";
6
6
  import { highlightToAnsi } from "../utils/highlightUtils.js";
7
+ import { renderMarkdownTable } from "../utils/markdownTable.js";
7
8
  const unescapeHtml = (html) => {
8
9
  return html
9
10
  .replace(/&amp;/g, "&")
@@ -14,6 +15,10 @@ const unescapeHtml = (html) => {
14
15
  .replace(/&apos;/g, "'");
15
16
  };
16
17
  class AnsiRenderer extends Renderer {
18
+ constructor(columns) {
19
+ super();
20
+ this.columns = columns;
21
+ }
17
22
  code({ text, lang }) {
18
23
  const prefix = lang ? `\`\`\`${lang}` : "```";
19
24
  const suffix = "```";
@@ -67,18 +72,8 @@ class AnsiRenderer extends Renderer {
67
72
  return `\n${text}\n`;
68
73
  }
69
74
  table(token) {
70
- const header = token.header.map((cell) => this.tablecell(cell)).join("");
71
- const body = token.rows
72
- .map((row) => row.map((cell) => this.tablecell(cell)).join("") + "\n")
73
- .join("");
74
- return `\n${header}\n${body}\n`;
75
- }
76
- tablerow({ text }) {
77
- return text + "\n";
78
- }
79
- tablecell(token) {
80
- const text = token.header ? chalk.bold(token.text) : token.text;
81
- return text + " | ";
75
+ const table = renderMarkdownTable(token, this.columns, (tokens) => this.parser.parseInline(tokens));
76
+ return `\n${table}\n`;
82
77
  }
83
78
  strong({ tokens }) {
84
79
  const text = this.parser.parseInline(tokens);
@@ -114,16 +109,18 @@ class AnsiRenderer extends Renderer {
114
109
  : unescapeHtml(token.text);
115
110
  }
116
111
  }
117
- const renderer = new AnsiRenderer();
112
+ const createRenderer = (columns) => new AnsiRenderer(columns);
118
113
  // Markdown component using custom ANSI renderer
119
114
  export const Markdown = React.memo(({ children }) => {
115
+ const { stdout } = useStdout();
116
+ const columns = stdout?.columns ?? 80;
120
117
  const ansiContent = useMemo(() => {
121
118
  return marked.parse(children, {
122
- renderer,
119
+ renderer: createRenderer(columns),
123
120
  gfm: true,
124
121
  breaks: true,
125
122
  });
126
- }, [children]);
123
+ }, [children, columns]);
127
124
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: ansiContent.trim() }) }));
128
125
  });
129
126
  // Add display name for debugging
@@ -6,5 +6,6 @@ export interface MessageListProps {
6
6
  forceStatic?: boolean;
7
7
  version?: string;
8
8
  workdir?: string;
9
+ showLoginHint?: boolean;
9
10
  }
10
- export declare const MessageList: React.MemoExoticComponent<({ messages, isExpanded, forceStatic, version, workdir, }: MessageListProps) => import("react/jsx-runtime").JSX.Element>;
11
+ export declare const MessageList: React.MemoExoticComponent<({ messages, isExpanded, forceStatic, version, workdir, showLoginHint, }: MessageListProps) => import("react/jsx-runtime").JSX.Element>;
@@ -5,7 +5,7 @@ import { Box, Text, Static } from "ink";
5
5
  import { MessageBlockItem } from "./MessageBlockItem.js";
6
6
  const MAX_MESSAGES_COLLAPSED = 30;
7
7
  const MAX_MESSAGES_EXPANDED = 10;
8
- export const MessageList = React.memo(({ messages, isExpanded = false, forceStatic = false, version, workdir, }) => {
8
+ export const MessageList = React.memo(({ messages, isExpanded = false, forceStatic = false, version, workdir, showLoginHint = false, }) => {
9
9
  const maxMessages = isExpanded
10
10
  ? MAX_MESSAGES_EXPANDED
11
11
  : MAX_MESSAGES_COLLAPSED;
@@ -74,7 +74,7 @@ export const MessageList = React.memo(({ messages, isExpanded = false, forceStat
74
74
  return null;
75
75
  }
76
76
  return (_jsx(MessageBlockItem, { block: item.block, message: item.message, isExpanded: isExpanded, paddingTop: 1 }, item.key));
77
- } })), dynamicBlocks.length > 0 && (_jsx(Box, { flexDirection: "column", children: dynamicBlocks.map((item) => (_jsx(MessageBlockItem, { block: item.block, message: item.message, isExpanded: isExpanded, paddingTop: 1 }, item.key))) }))] }));
77
+ } })), showLoginHint && _jsx(Text, { color: "gray", children: "Type /login to authenticate" }), dynamicBlocks.length > 0 && (_jsx(Box, { flexDirection: "column", children: dynamicBlocks.map((item) => (_jsx(MessageBlockItem, { block: item.block, message: item.message, isExpanded: isExpanded, paddingTop: 1 }, item.key))) }))] }));
78
78
  });
79
79
  // Add display name for debugging
80
80
  MessageList.displayName = "MessageList";
@@ -3,6 +3,7 @@ import { useState, useReducer, useEffect } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import { getMessageContent } from "wave-agent-sdk";
5
5
  import { rewindSelectorReducer } from "../reducers/rewindSelectorReducer.js";
6
+ import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
6
7
  export const RewindCommand = ({ messages: initialMessages, onSelect, onCancel, getFullMessageThread, }) => {
7
8
  const [messages, setMessages] = useState(initialMessages);
8
9
  const [isLoading, setIsLoading] = useState(!!getFullMessageThread);
@@ -14,10 +15,11 @@ export const RewindCommand = ({ messages: initialMessages, onSelect, onCancel, g
14
15
  });
15
16
  }
16
17
  }, [getFullMessageThread]);
17
- // Filter user messages as checkpoints, excluding meta messages
18
+ // Filter user messages as checkpoints, excluding meta messages and
19
+ // system-generated user-role messages (task notifications, hook injections)
18
20
  const checkpoints = messages
19
21
  .map((msg, index) => ({ msg, index }))
20
- .filter(({ msg }) => msg.role === "user" && !msg.isMeta);
22
+ .filter(({ msg }) => isUserCheckpointMessage(msg));
21
23
  const MAX_VISIBLE_ITEMS = 3;
22
24
  const [state, dispatch] = useReducer(rewindSelectorReducer, {
23
25
  selectedIndex: checkpoints.length - 1,
@@ -2,8 +2,6 @@ import React from "react";
2
2
  export interface StatusLineProps {
3
3
  permissionMode: string;
4
4
  isShellCommand: boolean;
5
- isGoalActive?: boolean;
6
- goalElapsed?: string;
7
5
  latestTotalTokens?: number;
8
6
  maxInputTokens?: number;
9
7
  }
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- export const StatusLine = ({ permissionMode, isShellCommand, isGoalActive, goalElapsed, latestTotalTokens = 0, maxInputTokens = 200000, }) => {
3
+ export const StatusLine = ({ permissionMode, isShellCommand, latestTotalTokens = 0, maxInputTokens = 200000, }) => {
4
4
  const percentage = latestTotalTokens > 0
5
5
  ? Math.min(Math.round((latestTotalTokens / maxInputTokens) * 100), 100)
6
6
  : 0;
7
7
  const contextColor = percentage > 95 ? "red" : percentage > 80 ? "yellow" : "gray";
8
- return (_jsxs(Box, { paddingRight: 1, justifyContent: "space-between", width: "100%", children: [isShellCommand ? (_jsxs(Text, { color: "gray", children: ["Shell: ", _jsx(Text, { color: "yellow", children: "Run shell command" })] })) : (_jsxs(Box, { children: [isGoalActive && (_jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "\u25CE /goal" }), " active", " ", goalElapsed && _jsxs(Text, { children: ["(", goalElapsed, ")"] }), " |", " "] })), _jsxs(Text, { color: "gray", children: ["Mode:", " ", _jsx(Text, { color: permissionMode === "plan"
9
- ? "yellow"
10
- : permissionMode === "bypassPermissions"
11
- ? "red"
12
- : "cyan", bold: permissionMode === "bypassPermissions", children: permissionMode }), " ", "(Shift+Tab to cycle)"] })] })), percentage > 0 && (_jsxs(Text, { color: contextColor, children: [percentage, "% context"] }))] }));
8
+ return (_jsxs(Box, { paddingRight: 1, justifyContent: "space-between", width: "100%", children: [isShellCommand ? (_jsxs(Text, { color: "gray", children: ["Shell: ", _jsx(Text, { color: "yellow", children: "Run shell command" })] })) : (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["Mode:", " ", _jsx(Text, { color: permissionMode === "plan"
9
+ ? "yellow"
10
+ : permissionMode === "bypassPermissions"
11
+ ? "red"
12
+ : "cyan", bold: permissionMode === "bypassPermissions", children: permissionMode }), " ", "(Shift+Tab to cycle)"] }) })), percentage > 0 && (_jsxs(Text, { color: contextColor, children: [percentage, "% context"] }))] }));
13
13
  };
@@ -75,7 +75,7 @@ function getDisplayLimit(rows) {
75
75
  }
76
76
  export const TaskList = () => {
77
77
  const tasks = useTasks();
78
- const { isTaskListVisible } = useChat();
78
+ const { isTaskListVisible, isBtwActive } = useChat();
79
79
  const { stdout } = useStdout();
80
80
  const completionTimestampsRef = React.useRef(new Map());
81
81
  const previousCompletedIdsRef = React.useRef(null);
@@ -156,6 +156,7 @@ export const TaskList = () => {
156
156
  }, [allCompleted, autoHidden, activeTasks.length]);
157
157
  if (tasks.length === 0 ||
158
158
  !isTaskListVisible ||
159
+ isBtwActive ||
159
160
  autoHidden ||
160
161
  (allCompleted && !hadIncompleteRef.current)) {
161
162
  return null;
@@ -4,5 +4,6 @@ interface ToolDisplayProps {
4
4
  block: ToolBlock;
5
5
  isExpanded?: boolean;
6
6
  }
7
+ export declare const getToolStatusColor: (stage: ToolBlock["stage"], success?: boolean, error?: string | Error) => string;
7
8
  export declare const ToolDisplay: React.FC<ToolDisplayProps>;
8
9
  export {};
@@ -2,19 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { getLastLines } from "wave-agent-sdk";
4
4
  import { DiffDisplay } from "./DiffDisplay.js";
5
+ // Status dot color for a tool block. In-flight stages (start/streaming/running)
6
+ // never show red — the outcome isn't known until the tool reaches "end".
7
+ // An explicit error overrides everything.
8
+ export const getToolStatusColor = (stage, success, error) => {
9
+ if (error)
10
+ return "red";
11
+ if (stage === "start" || stage === "streaming")
12
+ return "gray";
13
+ if (stage === "running")
14
+ return "yellow";
15
+ if (success)
16
+ return "green";
17
+ if (success === false)
18
+ return "red";
19
+ return "gray"; // Unknown state or no state information
20
+ };
5
21
  export const ToolDisplay = ({ block, isExpanded = false, }) => {
6
22
  const { parameters, result, compactParams, stage, success, error, name } = block;
7
23
  // Directly use compactParams
8
24
  // (no change needed as we destructured it above)
9
- const getStatusColor = () => {
10
- if (stage === "running")
11
- return "yellow";
12
- if (success)
13
- return "green";
14
- if (error || success === false)
15
- return "red";
16
- return "gray"; // Unknown state or no state information
17
- };
25
+ const getStatusColor = () => getToolStatusColor(stage, success, error);
18
26
  const hasImages = () => {
19
27
  return block.images && block.images.length > 0;
20
28
  };
@@ -77,10 +77,4 @@ export const AVAILABLE_COMMANDS = [
77
77
  description: "Compact conversation history to reduce context usage",
78
78
  handler: () => { },
79
79
  },
80
- {
81
- id: "goal",
82
- name: "goal",
83
- description: "Set, check, or clear an autonomous goal for the session",
84
- handler: () => { },
85
- },
86
80
  ];
@@ -9,16 +9,17 @@ export interface ChatContextType {
9
9
  isExpanded: boolean;
10
10
  isTaskListVisible: boolean;
11
11
  setIsTaskListVisible: (visible: boolean) => void;
12
+ isBtwActive: boolean;
13
+ setIsBtwActive: (active: boolean) => void;
12
14
  queuedMessages: QueuedMessage[];
13
15
  sessionId: string;
14
16
  sendMessage: (content: string, images?: Array<{
15
17
  path: string;
16
18
  mimeType: string;
17
19
  }>, longTextMap?: Record<string, string>) => Promise<void>;
18
- askBtw: (question: string) => Promise<string>;
20
+ askBtw: (question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void) => Promise<string>;
19
21
  clearMessages: () => Promise<void>;
20
22
  compact: (instructions?: string) => Promise<void>;
21
- goalCommand: (args?: string) => Promise<void>;
22
23
  abortMessage: () => void;
23
24
  recallQueuedMessage: () => QueuedMessage | null;
24
25
  removeQueuedMessageById: (id: string) => boolean;
@@ -74,9 +75,6 @@ export interface ChatContextType {
74
75
  workdir?: string;
75
76
  recreateAgent: () => void;
76
77
  triggerWorktreeRemoveHook: (worktreePath: string) => Promise<void>;
77
- isGoalActive: boolean;
78
- goalElapsed?: string;
79
- isGoalEvaluating: boolean;
80
78
  }
81
79
  export declare const useChat: () => ChatContextType;
82
80
  export interface ChatProviderProps extends BaseAppProps {