wave-code 1.0.0 → 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 (70) 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/InputBox.d.ts +0 -3
  6. package/dist/components/InputBox.js +11 -9
  7. package/dist/components/LoadingIndicator.d.ts +1 -2
  8. package/dist/components/LoadingIndicator.js +2 -2
  9. package/dist/components/Markdown.js +13 -16
  10. package/dist/components/MessageList.d.ts +2 -1
  11. package/dist/components/MessageList.js +2 -2
  12. package/dist/components/StatusLine.d.ts +0 -2
  13. package/dist/components/StatusLine.js +6 -6
  14. package/dist/components/TaskList.js +2 -1
  15. package/dist/components/ToolDisplay.d.ts +1 -0
  16. package/dist/components/ToolDisplay.js +17 -9
  17. package/dist/constants/commands.js +0 -6
  18. package/dist/contexts/useChat.d.ts +3 -5
  19. package/dist/contexts/useChat.js +242 -82
  20. package/dist/daemon-cli.d.ts +10 -0
  21. package/dist/daemon-cli.js +15 -0
  22. package/dist/hooks/useInputManager.js +99 -22
  23. package/dist/index.js +10 -0
  24. package/dist/managers/inputHandlers.js +50 -22
  25. package/dist/managers/inputReducer.d.ts +12 -2
  26. package/dist/managers/inputReducer.js +57 -9
  27. package/dist/stdio/agentBridge.d.ts +23 -0
  28. package/dist/stdio/agentBridge.js +126 -16
  29. package/dist/stdio/daemonServer.d.ts +67 -0
  30. package/dist/stdio/daemonServer.js +191 -0
  31. package/dist/stdio/index.d.ts +2 -0
  32. package/dist/stdio/index.js +2 -0
  33. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  34. package/dist/stdio/jsonRpcConnection.js +127 -0
  35. package/dist/stdio/protocol.d.ts +2 -2
  36. package/dist/stdio/stdioServer.d.ts +2 -7
  37. package/dist/stdio/stdioServer.js +9 -100
  38. package/dist/utils/bracketedPaste.d.ts +39 -0
  39. package/dist/utils/bracketedPaste.js +122 -0
  40. package/dist/utils/markdownTable.d.ts +34 -0
  41. package/dist/utils/markdownTable.js +302 -0
  42. package/dist/utils/throttle.d.ts +3 -3
  43. package/package.json +4 -2
  44. package/src/cli.tsx +20 -1
  45. package/src/components/App.tsx +5 -0
  46. package/src/components/BtwDisplay.tsx +36 -12
  47. package/src/components/ChatInterface.tsx +25 -12
  48. package/src/components/InputBox.tsx +10 -18
  49. package/src/components/LoadingIndicator.tsx +1 -4
  50. package/src/components/Markdown.tsx +15 -18
  51. package/src/components/MessageList.tsx +6 -0
  52. package/src/components/StatusLine.tsx +0 -10
  53. package/src/components/TaskList.tsx +2 -1
  54. package/src/components/ToolDisplay.tsx +17 -6
  55. package/src/constants/commands.ts +0 -6
  56. package/src/contexts/useChat.tsx +310 -95
  57. package/src/daemon-cli.ts +17 -0
  58. package/src/hooks/useInputManager.ts +108 -22
  59. package/src/index.ts +12 -0
  60. package/src/managers/inputHandlers.ts +49 -22
  61. package/src/managers/inputReducer.ts +66 -11
  62. package/src/stdio/agentBridge.ts +188 -17
  63. package/src/stdio/daemonServer.ts +212 -0
  64. package/src/stdio/index.ts +2 -0
  65. package/src/stdio/jsonRpcConnection.ts +160 -0
  66. package/src/stdio/protocol.ts +5 -2
  67. package/src/stdio/stdioServer.ts +14 -120
  68. package/src/utils/bracketedPaste.ts +170 -0
  69. package/src/utils/markdownTable.ts +359 -0
  70. package/src/utils/throttle.ts +8 -8
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, isCompacting: isCompacting, isGoalEvaluating: isGoalEvaluating, 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
  };
@@ -6,7 +6,6 @@ export interface InputBoxProps {
6
6
  isLoading?: boolean;
7
7
  isCommandRunning?: boolean;
8
8
  isCompacting?: boolean;
9
- isGoalEvaluating?: boolean;
10
9
  workdir?: string;
11
10
  sendMessage?: (message: string, images?: Array<{
12
11
  path: string;
@@ -20,7 +19,5 @@ export interface InputBoxProps {
20
19
  hasSlashCommand?: (commandId: string) => boolean;
21
20
  latestTotalTokens?: number;
22
21
  maxInputTokens?: number;
23
- isGoalActive?: boolean;
24
- goalElapsed?: string;
25
22
  }
26
23
  export declare const InputBox: React.FC<InputBoxProps>;
@@ -20,17 +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 = ({ isLoading, isCommandRunning, isCompacting, isGoalEvaluating, 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
28
  // Idle means no AI work in flight. Esc double-press clear only applies when
29
29
  // idle; while busy, Esc keeps its abort semantics.
30
- const isIdle = !(isLoading ||
31
- isCommandRunning ||
32
- isCompacting ||
33
- isGoalEvaluating);
30
+ const isIdle = !(isLoading || isCommandRunning || isCompacting);
34
31
  const onRecallQueuedMessage = useCallback(() => {
35
32
  const msg = recallQueuedMessage();
36
33
  if (msg) {
@@ -64,7 +61,6 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
64
61
  onAskBtw: askBtw,
65
62
  onClearMessages: clearMessages,
66
63
  onCompact: compact,
67
- onGoalCommand: goalCommand,
68
64
  onHasSlashCommand: hasSlashCommand,
69
65
  onAbortMessage: abortMessage,
70
66
  onBackgroundCurrentTask: backgroundCurrentTask,
@@ -80,6 +76,12 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
80
76
  useEffect(() => {
81
77
  setInputTextRef.current = setInputText;
82
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]);
83
85
  // Sync permission mode from useChat to InputManager
84
86
  useEffect(() => {
85
87
  setPermissionMode(chatPermissionMode);
@@ -146,7 +148,7 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
146
148
  if (showModelSelector) {
147
149
  return (_jsx(ModelSelector, { onCancel: () => setShowModelSelector(false), currentModel: currentModel, configuredModels: configuredModels, onSelectModel: setModel }));
148
150
  }
149
- 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
150
152
  ? null
151
153
  : showBackgroundTaskManager ||
152
154
  showMcpManager ||
@@ -155,5 +157,5 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
155
157
  showStatusCommand ||
156
158
  showLoginCommand ||
157
159
  showPluginManager ||
158
- 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, 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 })] }))] }));
159
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";
@@ -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 {