wave-code 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +20 -1
- package/dist/components/App.js +7 -0
- package/dist/components/BtwDisplay.js +13 -3
- package/dist/components/ChatInterface.js +25 -8
- package/dist/components/InputBox.d.ts +1 -3
- package/dist/components/InputBox.js +12 -9
- package/dist/components/LoadingIndicator.d.ts +1 -2
- package/dist/components/LoadingIndicator.js +2 -2
- package/dist/components/LoginCommand.js +4 -2
- package/dist/components/Markdown.js +13 -16
- package/dist/components/Notifications.d.ts +7 -0
- package/dist/components/Notifications.js +9 -0
- package/dist/components/StatusLine.d.ts +0 -4
- package/dist/components/StatusLine.js +6 -10
- package/dist/components/TaskList.js +2 -1
- package/dist/components/ToolDisplay.d.ts +1 -0
- package/dist/components/ToolDisplay.js +17 -9
- package/dist/constants/commands.js +0 -6
- package/dist/contexts/useChat.d.ts +4 -6
- package/dist/contexts/useChat.js +253 -110
- package/dist/daemon-cli.d.ts +10 -0
- package/dist/daemon-cli.js +15 -0
- package/dist/hooks/useInputManager.js +99 -22
- package/dist/index.js +10 -0
- package/dist/managers/inputHandlers.js +50 -22
- package/dist/managers/inputReducer.d.ts +12 -2
- package/dist/managers/inputReducer.js +57 -9
- package/dist/stdio/agentBridge.d.ts +23 -0
- package/dist/stdio/agentBridge.js +134 -16
- package/dist/stdio/daemonServer.d.ts +67 -0
- package/dist/stdio/daemonServer.js +191 -0
- package/dist/stdio/index.d.ts +2 -0
- package/dist/stdio/index.js +2 -0
- package/dist/stdio/jsonRpcConnection.d.ts +30 -0
- package/dist/stdio/jsonRpcConnection.js +127 -0
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/stdio/stdioServer.d.ts +2 -7
- package/dist/stdio/stdioServer.js +9 -100
- package/dist/utils/bracketedPaste.d.ts +39 -0
- package/dist/utils/bracketedPaste.js +122 -0
- package/dist/utils/markdownTable.d.ts +34 -0
- package/dist/utils/markdownTable.js +302 -0
- package/dist/utils/throttle.d.ts +3 -3
- package/package.json +4 -2
- package/src/cli.tsx +20 -1
- package/src/components/App.tsx +5 -0
- package/src/components/BtwDisplay.tsx +36 -12
- package/src/components/ChatInterface.tsx +30 -15
- package/src/components/InputBox.tsx +25 -24
- package/src/components/LoadingIndicator.tsx +1 -4
- package/src/components/LoginCommand.tsx +4 -2
- package/src/components/Markdown.tsx +15 -18
- package/src/components/Notifications.tsx +31 -0
- package/src/components/StatusLine.tsx +17 -44
- package/src/components/TaskList.tsx +2 -1
- package/src/components/ToolDisplay.tsx +17 -6
- package/src/constants/commands.ts +0 -6
- package/src/contexts/useChat.tsx +326 -140
- package/src/daemon-cli.ts +17 -0
- package/src/hooks/useInputManager.ts +108 -22
- package/src/index.ts +12 -0
- package/src/managers/inputHandlers.ts +49 -22
- package/src/managers/inputReducer.ts +66 -11
- package/src/stdio/agentBridge.ts +196 -17
- package/src/stdio/daemonServer.ts +212 -0
- package/src/stdio/index.ts +2 -0
- package/src/stdio/jsonRpcConnection.ts +160 -0
- package/src/stdio/protocol.ts +5 -2
- package/src/stdio/stdioServer.ts +14 -120
- package/src/utils/bracketedPaste.ts +170 -0
- package/src/utils/markdownTable.ts +359 -0
- 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
|
-
|
|
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) => {
|
package/dist/components/App.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,31 @@ 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,
|
|
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, forceRemount, 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
|
+
// An SSO token counts as authenticated even if the access token is stale —
|
|
23
|
+
// it refreshes lazily on the next API call (matching the claude-code CLI).
|
|
24
|
+
const computeAuthState = useCallback(() => {
|
|
25
|
+
if (authService.getSSOToken())
|
|
26
|
+
return true;
|
|
27
|
+
const gateway = getGatewayConfig();
|
|
28
|
+
return Boolean(gateway.apiKey || gateway.baseURL);
|
|
29
|
+
}, [getGatewayConfig]);
|
|
30
|
+
const [hasAuth, setHasAuth] = useState(computeAuthState);
|
|
31
|
+
// Keep the /login hint in sync with auth state changes (login/logout).
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const unsubscribe = authService.onAuthChange(() => {
|
|
34
|
+
setHasAuth(computeAuthState());
|
|
35
|
+
});
|
|
36
|
+
return unsubscribe;
|
|
37
|
+
}, [computeAuthState]);
|
|
38
|
+
const showLoginHint = !hasAuth;
|
|
19
39
|
// Handle forceStatic mode for overflow and request remount when exiting
|
|
20
40
|
useEffect(() => {
|
|
21
41
|
if (isConfirmationVisible && chatInterfaceRef.current) {
|
|
@@ -26,19 +46,16 @@ export const ChatInterface = () => {
|
|
|
26
46
|
}
|
|
27
47
|
else if (forceStatic && !hasPendingConfirmations) {
|
|
28
48
|
setForceStatic(false);
|
|
29
|
-
|
|
49
|
+
forceRemount();
|
|
30
50
|
}
|
|
31
51
|
}, [
|
|
32
52
|
isConfirmationVisible,
|
|
33
53
|
terminalHeight,
|
|
34
54
|
forceStatic,
|
|
35
55
|
hasPendingConfirmations,
|
|
36
|
-
|
|
56
|
+
forceRemount,
|
|
37
57
|
]);
|
|
38
58
|
if (!sessionId)
|
|
39
59
|
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 })] }))] }));
|
|
60
|
+
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 || 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, showLoginHint: showLoginHint })] })), 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
61
|
};
|
|
@@ -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,6 @@ export interface InputBoxProps {
|
|
|
20
19
|
hasSlashCommand?: (commandId: string) => boolean;
|
|
21
20
|
latestTotalTokens?: number;
|
|
22
21
|
maxInputTokens?: number;
|
|
23
|
-
|
|
24
|
-
goalElapsed?: string;
|
|
22
|
+
showLoginHint?: boolean;
|
|
25
23
|
}
|
|
26
24
|
export declare const InputBox: React.FC<InputBoxProps>;
|
|
@@ -15,22 +15,20 @@ import { PluginManagerShell } from "./PluginManagerShell.js";
|
|
|
15
15
|
import { ModelSelector } from "./ModelSelector.js";
|
|
16
16
|
import { WorkflowManager } from "./WorkflowManager.js";
|
|
17
17
|
import { StatusLine } from "./StatusLine.js";
|
|
18
|
+
import { Notifications } from "./Notifications.js";
|
|
18
19
|
import { BtwDisplay } from "./BtwDisplay.js";
|
|
19
20
|
import { useInputManager } from "../hooks/useInputManager.js";
|
|
20
21
|
import { useChat } from "../contexts/useChat.js";
|
|
21
22
|
export const INPUT_PLACEHOLDER_TEXT = "Type your message (use /help for more info)...";
|
|
22
23
|
export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(0, 10);
|
|
23
|
-
export const InputBox = ({ isLoading, isCommandRunning, isCompacting,
|
|
24
|
-
const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact,
|
|
24
|
+
export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessage = () => { }, abortMessage = () => { }, mcpServers = [], connectMcpServer = async () => false, disconnectMcpServer = async () => false, slashCommands = [], hasSlashCommand = () => false, latestTotalTokens = 0, maxInputTokens = 200000, showLoginHint = false, }) => {
|
|
25
|
+
const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, setIsBtwActive, } = useChat();
|
|
25
26
|
// Ref to hold setInputText so queue callbacks can access it before useInputManager returns
|
|
26
27
|
const setInputTextRef = useRef(() => { });
|
|
27
28
|
const hasQueuedMessages = (queuedMessages?.length ?? 0) > 0;
|
|
28
29
|
// Idle means no AI work in flight. Esc double-press clear only applies when
|
|
29
30
|
// idle; while busy, Esc keeps its abort semantics.
|
|
30
|
-
const isIdle = !(isLoading ||
|
|
31
|
-
isCommandRunning ||
|
|
32
|
-
isCompacting ||
|
|
33
|
-
isGoalEvaluating);
|
|
31
|
+
const isIdle = !(isLoading || isCommandRunning || isCompacting);
|
|
34
32
|
const onRecallQueuedMessage = useCallback(() => {
|
|
35
33
|
const msg = recallQueuedMessage();
|
|
36
34
|
if (msg) {
|
|
@@ -64,7 +62,6 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
|
|
|
64
62
|
onAskBtw: askBtw,
|
|
65
63
|
onClearMessages: clearMessages,
|
|
66
64
|
onCompact: compact,
|
|
67
|
-
onGoalCommand: goalCommand,
|
|
68
65
|
onHasSlashCommand: hasSlashCommand,
|
|
69
66
|
onAbortMessage: abortMessage,
|
|
70
67
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
@@ -80,6 +77,12 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
|
|
|
80
77
|
useEffect(() => {
|
|
81
78
|
setInputTextRef.current = setInputText;
|
|
82
79
|
}, [setInputText]);
|
|
80
|
+
// Sync the btw overlay's visibility to ChatContext so siblings (TaskList)
|
|
81
|
+
// can hide while the side-question is on display (aligned with Claude Code,
|
|
82
|
+
// which suppresses the expanded task list while a local-jsx command shows).
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setIsBtwActive(btwState.question !== "" || btwState.answer !== undefined);
|
|
85
|
+
}, [btwState.question, btwState.answer, setIsBtwActive]);
|
|
83
86
|
// Sync permission mode from useChat to InputManager
|
|
84
87
|
useEffect(() => {
|
|
85
88
|
setPermissionMode(chatPermissionMode);
|
|
@@ -146,7 +149,7 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
|
|
|
146
149
|
if (showModelSelector) {
|
|
147
150
|
return (_jsx(ModelSelector, { onCancel: () => setShowModelSelector(false), currentModel: currentModel, configuredModels: configuredModels, onSelectModel: setModel }));
|
|
148
151
|
}
|
|
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
|
|
152
|
+
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
153
|
? null
|
|
151
154
|
: showBackgroundTaskManager ||
|
|
152
155
|
showMcpManager ||
|
|
@@ -155,5 +158,5 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, isGoalEval
|
|
|
155
158
|
showStatusCommand ||
|
|
156
159
|
showLoginCommand ||
|
|
157
160
|
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,
|
|
161
|
+
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) }) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(StatusLine, { permissionMode: permissionMode, isShellCommand: isShellCommand }), _jsx(Notifications, { latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, showLoginHint: showLoginHint })] })] }))] }));
|
|
159
162
|
};
|
|
@@ -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,
|
|
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,
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", children: [isLoading && !isCompacting &&
|
|
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";
|
|
@@ -72,7 +72,9 @@ export const LoginCommand = ({ onCancel }) => {
|
|
|
72
72
|
const handleEnter = async () => {
|
|
73
73
|
if (isLoadingRef.current)
|
|
74
74
|
return;
|
|
75
|
-
|
|
75
|
+
// A stale-but-present SSO token still counts as logged in; it refreshes
|
|
76
|
+
// lazily on the next API call. Enter toggles logout only when a token exists.
|
|
77
|
+
const isAuthenticated = Boolean(authService.getSSOToken());
|
|
76
78
|
if (isAuthenticated) {
|
|
77
79
|
await authService.clearAuth();
|
|
78
80
|
setMessage("Logged out successfully");
|
|
@@ -112,7 +114,7 @@ export const LoginCommand = ({ onCancel }) => {
|
|
|
112
114
|
setIsLoading(false);
|
|
113
115
|
}
|
|
114
116
|
};
|
|
115
|
-
const isAuthenticated = authService.
|
|
117
|
+
const isAuthenticated = Boolean(authService.getSSOToken());
|
|
116
118
|
const token = authService.getSSOToken();
|
|
117
119
|
const serverUrl = authService.getServerUrl();
|
|
118
120
|
const truncatedToken = token && token.length > 14
|
|
@@ -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(/&/g, "&")
|
|
@@ -14,6 +15,10 @@ const unescapeHtml = (html) => {
|
|
|
14
15
|
.replace(/'/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
|
|
71
|
-
|
|
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
|
|
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export const Notifications = ({ latestTotalTokens = 0, maxInputTokens = 200000, showLoginHint = false, }) => {
|
|
4
|
+
const percentage = latestTotalTokens > 0
|
|
5
|
+
? Math.min(Math.round((latestTotalTokens / maxInputTokens) * 100), 100)
|
|
6
|
+
: 0;
|
|
7
|
+
const contextColor = percentage > 95 ? "red" : percentage > 80 ? "yellow" : "gray";
|
|
8
|
+
return (_jsxs(Box, { gap: 1, children: [showLoginHint && _jsx(Text, { color: "gray", children: "Type /login to authenticate" }), percentage > 0 && (_jsxs(Text, { color: contextColor, children: [percentage, "% context"] }))] }));
|
|
9
|
+
};
|
|
@@ -2,9 +2,5 @@ import React from "react";
|
|
|
2
2
|
export interface StatusLineProps {
|
|
3
3
|
permissionMode: string;
|
|
4
4
|
isShellCommand: boolean;
|
|
5
|
-
isGoalActive?: boolean;
|
|
6
|
-
goalElapsed?: string;
|
|
7
|
-
latestTotalTokens?: number;
|
|
8
|
-
maxInputTokens?: number;
|
|
9
5
|
}
|
|
10
6
|
export declare const StatusLine: React.FC<StatusLineProps>;
|
|
@@ -1,13 +1,9 @@
|
|
|
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,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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"] }))] }));
|
|
3
|
+
export const StatusLine = ({ permissionMode, isShellCommand, }) => {
|
|
4
|
+
return (_jsx(Box, { children: isShellCommand ? (_jsxs(Text, { color: "gray", children: ["Shell: ", _jsx(Text, { color: "yellow", children: "Run shell command" })] })) : (_jsxs(Text, { color: "gray", children: ["Mode:", " ", _jsx(Text, { color: permissionMode === "plan"
|
|
5
|
+
? "yellow"
|
|
6
|
+
: permissionMode === "bypassPermissions"
|
|
7
|
+
? "red"
|
|
8
|
+
: "cyan", bold: permissionMode === "bypassPermissions", children: permissionMode }), " ", "(Shift+Tab to cycle)"] })) }));
|
|
13
9
|
};
|
|
@@ -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;
|
|
@@ -61,7 +62,7 @@ export interface ChatContextType {
|
|
|
61
62
|
handleConfirmationCancel: () => void;
|
|
62
63
|
backgroundCurrentTask: () => void;
|
|
63
64
|
remountKey: number;
|
|
64
|
-
|
|
65
|
+
forceRemount: () => void;
|
|
65
66
|
handleRewindSelect: (index: number) => Promise<void>;
|
|
66
67
|
getFullMessageThread: () => Promise<{
|
|
67
68
|
messages: Message[];
|
|
@@ -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 {
|