wave-code 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle/wave.mjs +1382 -1131
- package/package.json +9 -7
- package/src/cli.tsx +14 -22
- package/src/commands/update.ts +62 -22
- package/src/components/AgentsManager.tsx +3 -3
- package/src/components/App.tsx +5 -0
- package/src/components/BackgroundTaskManager.tsx +76 -54
- package/src/components/BtwDisplay.tsx +71 -15
- package/src/components/CompactDisplay.tsx +1 -6
- package/src/components/ConfirmationDetails.tsx +34 -13
- package/src/components/ConfirmationSelector.tsx +31 -36
- package/src/components/DiffDisplay.tsx +1 -1
- package/src/components/HooksManager.tsx +3 -3
- package/src/components/InputBox.tsx +37 -11
- package/src/components/LoginCommand.tsx +16 -35
- package/src/components/Markdown.tsx +7 -3
- package/src/components/MarketplaceAddForm.tsx +15 -19
- package/src/components/MessageBlockItem.tsx +0 -6
- package/src/components/RemoteControlView.tsx +175 -0
- package/src/components/ScrolledContent.tsx +15 -13
- package/src/components/SkillsManager.tsx +3 -3
- package/src/components/TaskList.tsx +3 -3
- package/src/components/ToolDisplay.tsx +3 -18
- package/src/components/WorkflowManager.tsx +150 -136
- package/src/components/WorkflowRunDetail.tsx +193 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +308 -154
- package/src/daemon/commands.ts +31 -22
- package/src/daemon-cli.ts +6 -1
- package/src/hooks/useInputManager.ts +25 -39
- package/src/hooks/useLineScroll.ts +51 -5
- package/src/hooks/useRemoteControl.ts +346 -0
- package/src/index.ts +35 -0
- package/src/managers/inputHandlers.ts +5 -0
- package/src/managers/inputReducer.ts +39 -0
- package/src/print-cli.ts +5 -0
- package/src/reducers/workflowManagerReducer.ts +21 -2
- package/src/remote/frameBudget.ts +45 -0
- package/src/remote/hostIdentity.ts +128 -0
- package/src/remote/messageWindow.ts +46 -0
- package/src/remote/notificationCoalescer.ts +192 -0
- package/src/remote/relayClient.ts +690 -0
- package/src/remote/remoteControlCliHost.ts +420 -0
- package/src/remote/remoteControlCommands.ts +197 -0
- package/src/remote/remoteControlConfig.ts +353 -0
- package/src/remote/remoteControlService.ts +822 -0
- package/src/remote/remoteControlSink.ts +78 -0
- package/src/remote/remoteNotifications.ts +200 -0
- package/src/stdio/agentBridge.ts +874 -106
- package/src/stdio/daemonServer.ts +30 -0
- package/src/stdio/hostSlashCommands.ts +209 -0
- package/src/stdio/jsonRpcConnection.ts +11 -2
- package/src/stdio/protocol.ts +14 -115
- package/src/stdio/stdioServer.ts +33 -2
- package/src/stdio/uiPayload.ts +65 -0
- package/src/stdio-cli.ts +11 -2
- package/src/types.ts +7 -0
- package/src/utils/gitCredentialHelper.ts +106 -0
- package/src/utils/logger.ts +5 -2
- package/src/utils/rewindCheckpoints.ts +2 -4
- package/src/utils/workspaceDiff.ts +620 -0
- package/src/utils/worktree.ts +97 -0
- package/src/utils/bracketedPaste.ts +0 -170
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,11 +28,14 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.81",
|
|
31
32
|
"@vscode/ripgrep": "^1.18.0",
|
|
33
|
+
"qrcode": "^1.5.4",
|
|
32
34
|
"sharp": "^0.35.4"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
|
-
"@types/
|
|
37
|
+
"@types/qrcode": "^1.5.6",
|
|
38
|
+
"@types/react": "^19.3.0",
|
|
36
39
|
"@types/semver": "^7.7.1",
|
|
37
40
|
"@types/yargs": "^17.0.0",
|
|
38
41
|
"@vitest/coverage-v8": "^4.1.7",
|
|
@@ -43,12 +46,11 @@
|
|
|
43
46
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
44
47
|
"glob": "^13.0.0",
|
|
45
48
|
"highlight.js": "^11.11.1",
|
|
46
|
-
"ink": "^
|
|
47
|
-
"ink-testing-library": "^4.0.0",
|
|
49
|
+
"ink": "^8.0.0",
|
|
48
50
|
"marked": "^17.0.2",
|
|
49
51
|
"node-html-parser": "^7.0.2",
|
|
50
|
-
"react": "^19.
|
|
51
|
-
"react-dom": "19.
|
|
52
|
+
"react": "^19.3.1",
|
|
53
|
+
"react-dom": "19.3.1",
|
|
52
54
|
"rimraf": "^6.1.2",
|
|
53
55
|
"semver": "^7.7.4",
|
|
54
56
|
"string-width": "^8.2.2",
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"wrap-ansi": "^10.0.0",
|
|
58
60
|
"yargs": "^17.7.2",
|
|
59
61
|
"zod": "^3.23.8",
|
|
60
|
-
"wave-agent-sdk": "1.
|
|
62
|
+
"wave-agent-sdk": "1.5.0"
|
|
61
63
|
},
|
|
62
64
|
"engines": {
|
|
63
65
|
"node": ">=22"
|
package/src/cli.tsx
CHANGED
|
@@ -41,24 +41,13 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
41
41
|
// Continue with ink-based UI for normal mode
|
|
42
42
|
let shouldRemoveWorktree = false;
|
|
43
43
|
|
|
44
|
-
// Enable bracketed paste (DECSET 2004) so terminals wrap pasted text in
|
|
45
|
-
// \x1b[200~ ... \x1b[201~ markers. The input pipeline uses these to insert
|
|
46
|
-
// pasted text without submitting (a pasted trailing \r is not an Enter).
|
|
47
|
-
// Terminals without bracketed-paste support ignore the sequence and paste
|
|
48
|
-
// without markers, falling back to legacy behavior. No-op when stdout is
|
|
49
|
-
// not a TTY (piped input, stdio mode).
|
|
50
|
-
const stdoutIsTTY = process.stdout.isTTY === true;
|
|
51
|
-
if (stdoutIsTTY) {
|
|
52
|
-
process.stdout.write("\x1b[?2004h");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
44
|
const handleExit = (shouldRemove: boolean) => {
|
|
56
45
|
shouldRemoveWorktree = shouldRemove;
|
|
57
46
|
unmount();
|
|
58
47
|
};
|
|
59
48
|
|
|
60
49
|
// Render the application
|
|
61
|
-
const { unmount, waitUntilExit } = render(
|
|
50
|
+
const { clear, unmount, waitUntilExit } = render(
|
|
62
51
|
<App
|
|
63
52
|
restoreSessionId={restoreSessionId}
|
|
64
53
|
continueLastSession={continueLastSession}
|
|
@@ -75,21 +64,24 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
75
64
|
version={version}
|
|
76
65
|
model={model}
|
|
77
66
|
mcpServers={mcpServers}
|
|
67
|
+
// Late-bound: the hook is created before `render()` returns the instance
|
|
68
|
+
// whose `clear()` it calls.
|
|
69
|
+
onClearOutput={() => clear()}
|
|
78
70
|
onExit={handleExit}
|
|
79
71
|
/>,
|
|
80
|
-
{
|
|
72
|
+
{
|
|
73
|
+
exitOnCtrlC: false,
|
|
74
|
+
// Redraw only the lines that changed instead of rewriting the whole
|
|
75
|
+
// frame. Measured in a real terminal (tmux, 100x30) while a Bash command
|
|
76
|
+
// streamed 60 lines: 71 KB -> 20 KB written, 733 -> 78 cursor moves, same
|
|
77
|
+
// output on screen. ink falls back to whole-line updates for lines
|
|
78
|
+
// carrying controls it cannot diff around.
|
|
79
|
+
incrementalRendering: true,
|
|
80
|
+
},
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
// Wait for the app to finish unmounting
|
|
84
|
-
|
|
85
|
-
await waitUntilExit();
|
|
86
|
-
} finally {
|
|
87
|
-
// Disable bracketed paste (DECSET 2004) so the terminal returns to its
|
|
88
|
-
// previous paste handling.
|
|
89
|
-
if (stdoutIsTTY) {
|
|
90
|
-
process.stdout.write("\x1b[?2004l");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
84
|
+
await waitUntilExit();
|
|
93
85
|
|
|
94
86
|
try {
|
|
95
87
|
// Clean up old log files
|
package/src/commands/update.ts
CHANGED
|
@@ -1,32 +1,70 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "child_process";
|
|
2
|
-
import https from "https";
|
|
3
2
|
import chalk from "chalk";
|
|
4
3
|
import { isUpdateAvailable } from "../utils/version.js";
|
|
5
4
|
import { readNearestPackageJson } from "../utils/readPackageJson.js";
|
|
6
5
|
|
|
7
6
|
const currentVersion = readNearestPackageJson().version;
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
.on("error", (err) => {
|
|
27
|
-
reject(err);
|
|
28
|
-
});
|
|
8
|
+
/** npm registry mirror for China users (the CLI downloads its runtime deps from the same one). */
|
|
9
|
+
const REGISTRY_MIRROR = "https://registry.npmmirror.com";
|
|
10
|
+
/** Official registry — the fallback when the mirror is unreachable. */
|
|
11
|
+
const REGISTRY_OFFICIAL = "https://registry.npmjs.org";
|
|
12
|
+
/** Cap each registry request so a hanging one cannot stall the fallback. */
|
|
13
|
+
const REGISTRY_TIMEOUT_MS = 5000;
|
|
14
|
+
|
|
15
|
+
interface LatestVersion {
|
|
16
|
+
version: string;
|
|
17
|
+
/** The registry that answered — the install reuses it (see getLatestVersion). */
|
|
18
|
+
registry: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fetchLatestVersion(registry: string): Promise<string> {
|
|
22
|
+
const response = await fetch(`${registry}/wave-code/latest`, {
|
|
23
|
+
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
|
|
29
24
|
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`HTTP ${response.status} fetching ${registry}/wave-code/latest`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const json = (await response.json()) as { version?: string };
|
|
31
|
+
if (!json.version) {
|
|
32
|
+
throw new Error(`No version in the response from ${registry}`);
|
|
33
|
+
}
|
|
34
|
+
return json.version;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The mirror answers first so the check is fast in CN; the official registry is
|
|
39
|
+
* a silent fallback so an unreachable mirror never blocks the check.
|
|
40
|
+
*
|
|
41
|
+
* The install reuses whichever registry answered: a machine that can only reach
|
|
42
|
+
* the official registry would fail the install for the very reason the check
|
|
43
|
+
* fell back.
|
|
44
|
+
*/
|
|
45
|
+
async function getLatestVersion(): Promise<LatestVersion> {
|
|
46
|
+
try {
|
|
47
|
+
return {
|
|
48
|
+
version: await fetchLatestVersion(REGISTRY_MIRROR),
|
|
49
|
+
registry: REGISTRY_MIRROR,
|
|
50
|
+
};
|
|
51
|
+
} catch (mirrorError) {
|
|
52
|
+
try {
|
|
53
|
+
return {
|
|
54
|
+
version: await fetchLatestVersion(REGISTRY_OFFICIAL),
|
|
55
|
+
registry: REGISTRY_OFFICIAL,
|
|
56
|
+
};
|
|
57
|
+
} catch (officialError) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Could not reach ${REGISTRY_MIRROR} (${errorText(mirrorError)}) ` +
|
|
60
|
+
`or ${REGISTRY_OFFICIAL} (${errorText(officialError)})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function errorText(error: unknown): string {
|
|
67
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
function detectPackageManager(): "npm" | "pnpm" | "yarn" {
|
|
@@ -53,7 +91,7 @@ export async function updateCommand() {
|
|
|
53
91
|
console.log(chalk.dim(`Current version: ${currentVersion}`));
|
|
54
92
|
|
|
55
93
|
try {
|
|
56
|
-
const latestVersion = await getLatestVersion();
|
|
94
|
+
const { version: latestVersion, registry } = await getLatestVersion();
|
|
57
95
|
console.log(chalk.dim(`Latest version: ${latestVersion}`));
|
|
58
96
|
|
|
59
97
|
if (!isUpdateAvailable(currentVersion, latestVersion)) {
|
|
@@ -79,6 +117,8 @@ export async function updateCommand() {
|
|
|
79
117
|
updateCmd = "npm";
|
|
80
118
|
args = ["install", "-g", "wave-code@latest"];
|
|
81
119
|
}
|
|
120
|
+
// Appended once so every package manager (including a future branch) gets it.
|
|
121
|
+
args.push(`--registry=${registry}`);
|
|
82
122
|
|
|
83
123
|
console.log(chalk.blue(`Updating WAVE Code using ${packageManager}...`));
|
|
84
124
|
console.log(chalk.dim(`Running: ${updateCmd} ${args.join(" ")}`));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useReducer } from "react";
|
|
2
|
-
import { Box, Text, useInput,
|
|
2
|
+
import { Box, Text, useInput, useWindowSize } from "ink";
|
|
3
3
|
import type { SubagentConfiguration } from "wave-agent-sdk";
|
|
4
4
|
import { Markdown } from "./Markdown.js";
|
|
5
5
|
import {
|
|
@@ -47,7 +47,7 @@ export const AgentsManager: React.FC<AgentsManagerProps> = ({
|
|
|
47
47
|
agentDefinitions,
|
|
48
48
|
}) => {
|
|
49
49
|
const [state, dispatch] = useReducer(agentsManagerReducer, initialState);
|
|
50
|
-
const {
|
|
50
|
+
const { rows } = useWindowSize();
|
|
51
51
|
|
|
52
52
|
// Handle pending effects
|
|
53
53
|
useEffect(() => {
|
|
@@ -107,7 +107,7 @@ export const AgentsManager: React.FC<AgentsManagerProps> = ({
|
|
|
107
107
|
// Window slice: center the selected item within the visible area, clamping
|
|
108
108
|
// to the terminal's available rows (reusable pattern from
|
|
109
109
|
// BackgroundTaskManager).
|
|
110
|
-
const availableRows =
|
|
110
|
+
const availableRows = rows || 24;
|
|
111
111
|
const maxVisible = Math.max(3, Math.min(15, availableRows - 12));
|
|
112
112
|
const selectedFlatIndex = entries.findIndex(
|
|
113
113
|
(e) => e.selectableIndex === state.selectedIndex,
|
package/src/components/App.tsx
CHANGED
|
@@ -102,6 +102,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
|
102
102
|
version,
|
|
103
103
|
model,
|
|
104
104
|
mcpServers,
|
|
105
|
+
onClearOutput,
|
|
105
106
|
onExit,
|
|
106
107
|
}) => {
|
|
107
108
|
// Handle Ctrl-C for non-worktree sessions (immediate exit)
|
|
@@ -131,6 +132,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
|
131
132
|
version={version}
|
|
132
133
|
model={model}
|
|
133
134
|
mcpServers={mcpServers}
|
|
135
|
+
onClearOutput={onClearOutput}
|
|
134
136
|
>
|
|
135
137
|
<ChatWithExitPrompt worktreeSession={worktreeSession} onExit={onExit} />
|
|
136
138
|
</ChatProvider>
|
|
@@ -152,6 +154,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
|
152
154
|
version={version}
|
|
153
155
|
model={model}
|
|
154
156
|
mcpServers={mcpServers}
|
|
157
|
+
onClearOutput={onClearOutput}
|
|
155
158
|
>
|
|
156
159
|
<ChatInterface />
|
|
157
160
|
</ChatProvider>
|
|
@@ -174,6 +177,7 @@ export const App: React.FC<AppProps> = ({
|
|
|
174
177
|
version,
|
|
175
178
|
model,
|
|
176
179
|
mcpServers,
|
|
180
|
+
onClearOutput,
|
|
177
181
|
onExit,
|
|
178
182
|
}) => {
|
|
179
183
|
return (
|
|
@@ -195,6 +199,7 @@ export const App: React.FC<AppProps> = ({
|
|
|
195
199
|
version={version}
|
|
196
200
|
model={model}
|
|
197
201
|
mcpServers={mcpServers}
|
|
202
|
+
onClearOutput={onClearOutput}
|
|
198
203
|
onExit={onExit}
|
|
199
204
|
/>
|
|
200
205
|
</AppProvider>
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
backgroundTaskManagerReducer,
|
|
7
7
|
type BackgroundTaskManagerState,
|
|
8
8
|
} from "../reducers/backgroundTaskManagerReducer.js";
|
|
9
|
+
import WorkflowRunDetail, {
|
|
10
|
+
formatDuration,
|
|
11
|
+
formatTime,
|
|
12
|
+
} from "./WorkflowRunDetail.js";
|
|
9
13
|
|
|
10
14
|
export interface BackgroundTaskManagerProps {
|
|
11
15
|
onCancel: () => void;
|
|
@@ -14,8 +18,12 @@ export interface BackgroundTaskManagerProps {
|
|
|
14
18
|
export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
15
19
|
onCancel,
|
|
16
20
|
}) => {
|
|
17
|
-
const {
|
|
18
|
-
|
|
21
|
+
const {
|
|
22
|
+
backgroundTasks,
|
|
23
|
+
workflowRuns,
|
|
24
|
+
getBackgroundTaskOutput,
|
|
25
|
+
stopBackgroundTask,
|
|
26
|
+
} = useChat();
|
|
19
27
|
const MAX_VISIBLE_ITEMS = 3;
|
|
20
28
|
|
|
21
29
|
const [state, dispatch] = useReducer(backgroundTaskManagerReducer, {
|
|
@@ -71,18 +79,6 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
|
71
79
|
}
|
|
72
80
|
}, [viewMode, detailTaskId, getBackgroundTaskOutput]);
|
|
73
81
|
|
|
74
|
-
const formatDuration = (ms: number): string => {
|
|
75
|
-
if (ms < 1000) return `${ms}ms`;
|
|
76
|
-
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
77
|
-
const minutes = Math.floor(ms / 60000);
|
|
78
|
-
const seconds = Math.round((ms % 60000) / 1000);
|
|
79
|
-
return `${minutes}m ${seconds}s`;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const formatTime = (timestamp: number): string => {
|
|
83
|
-
return new Date(timestamp).toLocaleTimeString();
|
|
84
|
-
};
|
|
85
|
-
|
|
86
82
|
// Calculate visible window
|
|
87
83
|
const startIndex = Math.max(
|
|
88
84
|
0,
|
|
@@ -104,6 +100,15 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
|
104
100
|
return null;
|
|
105
101
|
}
|
|
106
102
|
|
|
103
|
+
// A workflow background task's id IS its run id, so a workflow task can show
|
|
104
|
+
// real progress instead of the (always empty) text output: the same run
|
|
105
|
+
// fields `/workflows` shows, via the same component. Absent run (e.g. the
|
|
106
|
+
// session was cleared) falls back to the plain task detail below.
|
|
107
|
+
const selectedRun =
|
|
108
|
+
task.type === "workflow"
|
|
109
|
+
? (workflowRuns.find((r) => r.runId === task.id) ?? null)
|
|
110
|
+
: null;
|
|
111
|
+
|
|
107
112
|
return (
|
|
108
113
|
<Box
|
|
109
114
|
flexDirection="column"
|
|
@@ -134,53 +139,70 @@ export const BackgroundTaskManager: React.FC<BackgroundTaskManagerProps> = ({
|
|
|
134
139
|
</Text>
|
|
135
140
|
</Box>
|
|
136
141
|
)}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<Box>
|
|
144
|
-
<Text>
|
|
145
|
-
<Text color="blue">Started:</Text> {formatTime(task.startTime)}
|
|
146
|
-
{task.runtime !== undefined && (
|
|
142
|
+
{/* With the run block below, the task's own status and start time
|
|
143
|
+
would repeat it — the run is the authority on how far the
|
|
144
|
+
workflow got. The header keeps only what identifies the task. */}
|
|
145
|
+
{!selectedRun && (
|
|
146
|
+
<>
|
|
147
|
+
<Box>
|
|
147
148
|
<Text>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
<Text color="blue">Status:</Text> {task.status}
|
|
150
|
+
{task.exitCode !== undefined &&
|
|
151
|
+
` (exit code: ${task.exitCode})`}
|
|
152
|
+
</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
<Box>
|
|
155
|
+
<Text>
|
|
156
|
+
<Text color="blue">Started:</Text>{" "}
|
|
157
|
+
{formatTime(task.startTime)}
|
|
158
|
+
{task.runtime !== undefined && (
|
|
159
|
+
<Text>
|
|
160
|
+
{" "}
|
|
161
|
+
| <Text color="blue">Runtime:</Text>{" "}
|
|
162
|
+
{formatDuration(task.runtime)}
|
|
163
|
+
</Text>
|
|
164
|
+
)}
|
|
151
165
|
</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
{task.outputPath && (
|
|
168
|
+
<Box>
|
|
169
|
+
<Text>
|
|
170
|
+
<Text color="blue">Log File:</Text> {task.outputPath}
|
|
171
|
+
</Text>
|
|
172
|
+
</Box>
|
|
152
173
|
)}
|
|
153
|
-
|
|
154
|
-
</Box>
|
|
155
|
-
{task.outputPath && (
|
|
156
|
-
<Box>
|
|
157
|
-
<Text>
|
|
158
|
-
<Text color="blue">Log File:</Text> {task.outputPath}
|
|
159
|
-
</Text>
|
|
160
|
-
</Box>
|
|
174
|
+
</>
|
|
161
175
|
)}
|
|
162
176
|
</Box>
|
|
163
177
|
|
|
164
|
-
{
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
{selectedRun ? (
|
|
179
|
+
<WorkflowRunDetail run={selectedRun} />
|
|
180
|
+
) : (
|
|
181
|
+
<>
|
|
182
|
+
{detailOutput.stdout && (
|
|
183
|
+
<Box flexDirection="column" marginTop={1}>
|
|
184
|
+
<Text color="green" bold>
|
|
185
|
+
OUTPUT (last 10 lines):
|
|
186
|
+
</Text>
|
|
187
|
+
<Box borderStyle="single" borderColor="green" padding={1}>
|
|
188
|
+
<Text>{getLastLines(detailOutput.stdout, 10)}</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
</Box>
|
|
191
|
+
)}
|
|
174
192
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
{detailOutput.stderr && (
|
|
194
|
+
<Box flexDirection="column" marginTop={1}>
|
|
195
|
+
<Text color="red" bold>
|
|
196
|
+
ERRORS:
|
|
197
|
+
</Text>
|
|
198
|
+
<Box borderStyle="single" borderColor="red" padding={1}>
|
|
199
|
+
<Text color="red">
|
|
200
|
+
{getLastLines(detailOutput.stderr, 10)}
|
|
201
|
+
</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
</Box>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
184
206
|
)}
|
|
185
207
|
|
|
186
208
|
<Box marginTop={1}>
|
|
@@ -1,22 +1,75 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { Markdown } from "./Markdown.js";
|
|
4
|
+
import { ScrolledContent } from "./ScrolledContent.js";
|
|
5
|
+
import { useLineScroll } from "../hooks/useLineScroll.js";
|
|
4
6
|
import { BtwState } from "../managers/inputReducer.js";
|
|
5
7
|
import { streamingTail } from "../utils/streamingText.js";
|
|
6
8
|
|
|
9
|
+
/** Rows moved by one ↑/↓ press (aligned with Claude Code's btw pager). */
|
|
10
|
+
export const BTW_SCROLL_LINES = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Rows the overlay leaves free inside the terminal so the frame never outgrows
|
|
13
|
+
* it: its own chrome (top margin, question line, content margin, hint = 4) plus
|
|
14
|
+
* the rows around it that must stay on screen (the streaming tail above, and
|
|
15
|
+
* the input box + status line below = 4), plus slack for a wrapped question.
|
|
16
|
+
* Claude Code reserves the same 11 rows (CHROME_ROWS 5 + OUTER_CHROME_ROWS 6).
|
|
17
|
+
*/
|
|
18
|
+
export const BTW_CHROME_ROWS = 11;
|
|
19
|
+
/** Smallest usable answer viewport, whatever the terminal size. */
|
|
20
|
+
export const BTW_MIN_VIEWPORT_ROWS = 5;
|
|
21
|
+
/** Footer hint with scrolling enabled — shown only while the answer overflows. */
|
|
22
|
+
export const BTW_SCROLL_HINT = "↑/↓ to scroll · Esc to dismiss";
|
|
23
|
+
/** Footer hint for an answer that fits — unchanged from before scrolling. */
|
|
24
|
+
export const BTW_DISMISS_HINT = "Escape to dismiss";
|
|
25
|
+
|
|
7
26
|
interface BtwDisplayProps {
|
|
8
27
|
btwState: BtwState;
|
|
28
|
+
/** Terminal rows. Defaults to the live window size; tests pass it explicitly
|
|
29
|
+
* so the viewport budget is deterministic. */
|
|
30
|
+
rows?: number;
|
|
9
31
|
}
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `/btw` overlay: the question line, the answer (Markdown, or the loading
|
|
35
|
+
* indicator while it streams) and the dismiss hint.
|
|
36
|
+
*
|
|
37
|
+
* The answer is rendered inside a fixed-height viewport rather than in the
|
|
38
|
+
* frame's natural height, so a long answer cannot push the top of the terminal
|
|
39
|
+
* out of reach: it scrolls with ↑/↓ (and Ctrl+P/Ctrl+N) while it overflows,
|
|
40
|
+
* and the viewport is recomputed from the terminal size on resize. Esc — and
|
|
41
|
+
* only Esc — dismisses the overlay.
|
|
42
|
+
*/
|
|
43
|
+
export const BtwDisplay: React.FC<BtwDisplayProps> = ({ btwState, rows }) => {
|
|
44
|
+
const { rows: terminalRows } = useWindowSize();
|
|
45
|
+
const availableRows = rows ?? terminalRows;
|
|
46
|
+
const viewportRows = Math.max(
|
|
47
|
+
BTW_MIN_VIEWPORT_ROWS,
|
|
48
|
+
availableRows - BTW_CHROME_ROWS,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const answer = btwState.answer ?? "";
|
|
52
|
+
|
|
53
|
+
// Rows really occupied by the answer (wrapped rows included), measured after
|
|
54
|
+
// layout by <ScrolledContent> — see that component for why it is measured
|
|
55
|
+
// instead of counted from the Markdown source. Zero while the answer is still
|
|
56
|
+
// streaming: the loading line is a single row and has nothing to scroll.
|
|
57
|
+
const [measuredRows, setMeasuredRows] = useState(0);
|
|
58
|
+
const contentRows = btwState.isLoading ? 0 : measuredRows;
|
|
59
|
+
|
|
60
|
+
const { scrollOffset, canScroll } = useLineScroll({
|
|
61
|
+
totalLines: contentRows,
|
|
62
|
+
visibleCount: viewportRows,
|
|
63
|
+
lineStep: BTW_SCROLL_LINES,
|
|
64
|
+
resetKey: btwState.question,
|
|
65
|
+
});
|
|
66
|
+
|
|
12
67
|
// Rendered for a real question (loading or answered) and for the bare
|
|
13
68
|
// `/btw` usage message (question === "", answer set).
|
|
14
69
|
if (!btwState.question && !btwState.answer) {
|
|
15
70
|
return null;
|
|
16
71
|
}
|
|
17
72
|
|
|
18
|
-
const answer = btwState.answer ?? "";
|
|
19
|
-
|
|
20
73
|
// The SDK surfaces failure as the answer string; classify by prefix so it
|
|
21
74
|
// renders in error color (aligned with Claude Code's error display).
|
|
22
75
|
const isError =
|
|
@@ -46,20 +99,23 @@ export const BtwDisplay: React.FC<BtwDisplayProps> = ({ btwState }) => {
|
|
|
46
99
|
</Text>
|
|
47
100
|
)}
|
|
48
101
|
</Box>
|
|
49
|
-
) : isError ? (
|
|
50
|
-
<Text color="error">{answer}</Text>
|
|
51
102
|
) : (
|
|
52
|
-
<
|
|
103
|
+
<ScrolledContent
|
|
104
|
+
visibleRows={viewportRows}
|
|
105
|
+
scrollOffset={scrollOffset}
|
|
106
|
+
onMeasuredRows={setMeasuredRows}
|
|
107
|
+
>
|
|
108
|
+
{isError ? (
|
|
109
|
+
<Text color="error">{answer}</Text>
|
|
110
|
+
) : (
|
|
111
|
+
<Markdown>{answer}</Markdown>
|
|
112
|
+
)}
|
|
113
|
+
</ScrolledContent>
|
|
53
114
|
)}
|
|
54
115
|
</Box>
|
|
55
|
-
{btwState.
|
|
56
|
-
<Box marginTop={1}>
|
|
57
|
-
<Text dimColor>Escape to dismiss</Text>
|
|
58
|
-
</Box>
|
|
59
|
-
)}
|
|
60
|
-
{!btwState.question && btwState.answer && (
|
|
116
|
+
{btwState.answer && (
|
|
61
117
|
<Box marginTop={1}>
|
|
62
|
-
<Text dimColor>
|
|
118
|
+
<Text dimColor>{canScroll ? BTW_SCROLL_HINT : BTW_DISMISS_HINT}</Text>
|
|
63
119
|
</Box>
|
|
64
120
|
)}
|
|
65
121
|
</Box>
|
|
@@ -26,12 +26,7 @@ export const CompactDisplay: React.FC<CompactDisplayProps> = ({ block }) => {
|
|
|
26
26
|
|
|
27
27
|
{content && (
|
|
28
28
|
<Box marginTop={1} flexDirection="column">
|
|
29
|
-
<Box
|
|
30
|
-
paddingLeft={2}
|
|
31
|
-
borderLeft
|
|
32
|
-
borderColor="gray"
|
|
33
|
-
flexDirection="column"
|
|
34
|
-
>
|
|
29
|
+
<Box paddingLeft={2} flexDirection="column">
|
|
35
30
|
<Text color="white">{displayContent}</Text>
|
|
36
31
|
</Box>
|
|
37
32
|
</Box>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Box, Text, useInput,
|
|
2
|
+
import { Box, Text, useInput, useWindowSize } from "ink";
|
|
3
3
|
import {
|
|
4
4
|
BASH_TOOL_NAME,
|
|
5
5
|
EDIT_TOOL_NAME,
|
|
@@ -36,11 +36,37 @@ const getActionDescription = (
|
|
|
36
36
|
return "Enter plan mode for complex task planning";
|
|
37
37
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
38
38
|
return "Answer questions to clarify intent";
|
|
39
|
-
case ARTIFACT_TOOL_NAME:
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
case ARTIFACT_TOOL_NAME: {
|
|
40
|
+
// `action` omitted means publish, so the publish wording must not be the
|
|
41
|
+
// switch's fallback: a comment or asset action added later would then be
|
|
42
|
+
// announced as "Publish file:".
|
|
43
|
+
const action = toolInput.action ?? "publish";
|
|
44
|
+
const target = toolInput.url || toolInput.file_path || "unknown artifact";
|
|
45
|
+
switch (action) {
|
|
46
|
+
case "list":
|
|
47
|
+
return `List artifacts (scope: ${toolInput.scope || "mine"})`;
|
|
48
|
+
case "upload_asset":
|
|
49
|
+
return `Upload asset: ${toolInput.file_path || "unknown file"}`;
|
|
50
|
+
case "list_assets":
|
|
51
|
+
return `List assets of artifact: ${target}`;
|
|
52
|
+
case "read_asset":
|
|
53
|
+
return `Read asset: ${toolInput.asset_id || "unknown asset"}`;
|
|
54
|
+
case "delete_asset":
|
|
55
|
+
return `Delete asset: ${toolInput.asset_id || "unknown asset"}`;
|
|
56
|
+
case "copy_from":
|
|
57
|
+
return `Copy assets into artifact: ${target}`;
|
|
58
|
+
case "read":
|
|
59
|
+
return `Read artifact: ${target}`;
|
|
60
|
+
case "reply":
|
|
61
|
+
return `Reply to comment thread ${toolInput.thread_id || "unknown thread"} on: ${target}`;
|
|
62
|
+
case "resolve":
|
|
63
|
+
return `${toolInput.resolved === false ? "Reopen" : "Resolve"} comment thread ${toolInput.thread_id || "unknown thread"} on: ${target}`;
|
|
64
|
+
case "publish":
|
|
65
|
+
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
66
|
+
default:
|
|
67
|
+
return `Artifact action "${String(action)}" on: ${target}`;
|
|
42
68
|
}
|
|
43
|
-
|
|
69
|
+
}
|
|
44
70
|
default:
|
|
45
71
|
return "Execute operation";
|
|
46
72
|
}
|
|
@@ -75,8 +101,8 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
75
101
|
|
|
76
102
|
// Terminal width is needed to render the plan's Markdown (table column widths
|
|
77
103
|
// are computed from it), matching <Markdown>'s own default.
|
|
78
|
-
const {
|
|
79
|
-
const columns =
|
|
104
|
+
const { columns: terminalColumns } = useWindowSize();
|
|
105
|
+
const columns = terminalColumns || 80;
|
|
80
106
|
|
|
81
107
|
const headerRows = 2 + (warning ? 1 : 0);
|
|
82
108
|
|
|
@@ -111,12 +137,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
111
137
|
? highlightToAnsi(JSON.stringify(toolInput, null, 2), "json")
|
|
112
138
|
.split("\n")
|
|
113
139
|
.map((line, index) => (
|
|
114
|
-
<Box
|
|
115
|
-
key={`json-${index}`}
|
|
116
|
-
paddingLeft={2}
|
|
117
|
-
borderLeft
|
|
118
|
-
borderColor="cyan"
|
|
119
|
-
>
|
|
140
|
+
<Box key={`json-${index}`} paddingLeft={2}>
|
|
120
141
|
<Text>{line || " "}</Text>
|
|
121
142
|
</Box>
|
|
122
143
|
))
|