wave-code 1.2.0 → 1.4.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 +1738 -893
- package/package.json +6 -3
- package/src/cli.tsx +9 -0
- package/src/commands/plugin/install.ts +6 -7
- package/src/commands/plugin/uninstall.ts +11 -5
- package/src/commands/update.ts +62 -22
- package/src/components/BackgroundTaskManager.tsx +76 -54
- package/src/components/ChatInterface.tsx +1 -0
- package/src/components/ConfirmationDetails.tsx +51 -16
- package/src/components/ConfirmationSelector.tsx +39 -0
- package/src/components/InputBox.tsx +41 -2
- package/src/components/Markdown.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +10 -27
- package/src/components/MarketplaceView.tsx +17 -3
- package/src/components/MessageBlockItem.tsx +0 -6
- package/src/components/Notifications.tsx +7 -0
- package/src/components/PlanView.tsx +22 -17
- package/src/components/PluginManagerShell.tsx +3 -3
- package/src/components/PluginManagerTypes.ts +2 -1
- package/src/components/ResumeCommand.tsx +84 -0
- package/src/components/ScrolledContent.tsx +71 -0
- package/src/components/SessionSelector.tsx +184 -6
- package/src/components/SessionSelectorFlow.tsx +246 -0
- package/src/components/WorkflowManager.tsx +38 -138
- package/src/components/WorkflowRunDetail.tsx +193 -0
- package/src/constants/commands.ts +24 -0
- package/src/contexts/useChat.tsx +517 -155
- package/src/daemon/commands.ts +582 -50
- package/src/daemon-cli.ts +12 -1
- package/src/hooks/useInputManager.ts +20 -0
- package/src/hooks/usePluginManager.ts +104 -78
- package/src/hooks/useRemoteControl.ts +324 -0
- package/src/index.ts +97 -8
- package/src/managers/inputHandlers.ts +16 -9
- package/src/managers/inputReducer.ts +30 -9
- package/src/print-cli.ts +5 -0
- package/src/reducers/confirmationReducer.ts +20 -1
- package/src/reducers/selectorReducer.ts +3 -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 +97 -0
- package/src/remote/notificationCoalescer.ts +174 -0
- package/src/remote/relayClient.ts +668 -0
- package/src/remote/remoteControlCliHost.ts +391 -0
- package/src/remote/remoteControlCommands.ts +197 -0
- package/src/remote/remoteControlConfig.ts +334 -0
- package/src/remote/remoteControlService.ts +799 -0
- package/src/remote/remoteControlSink.ts +77 -0
- package/src/remote/remoteNotifications.ts +194 -0
- package/src/session-selector-cli.tsx +9 -255
- package/src/stdio/agentBridge.ts +1001 -165
- package/src/stdio/daemonServer.ts +30 -0
- package/src/stdio/jsonRpcConnection.ts +11 -2
- package/src/stdio/protocol.ts +14 -106
- package/src/stdio/stdioServer.ts +33 -2
- package/src/stdio-cli.ts +16 -1
- package/src/utils/gitCredentialHelper.ts +106 -0
- package/src/utils/logger.ts +5 -2
- package/src/utils/rewindCheckpoints.ts +2 -4
- package/src/utils/sessionOwnership.ts +62 -0
- package/src/utils/workspaceDiff.ts +620 -0
- package/src/utils/worktree.ts +97 -0
- package/src/components/PlanDisplay.tsx +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,9 +28,12 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@vscode/ripgrep": "^1.18.0"
|
|
31
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
32
|
+
"qrcode": "^1.5.4",
|
|
33
|
+
"sharp": "^0.35.4"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
36
|
+
"@types/qrcode": "^1.5.6",
|
|
34
37
|
"@types/react": "^19.1.8",
|
|
35
38
|
"@types/semver": "^7.7.1",
|
|
36
39
|
"@types/yargs": "^17.0.0",
|
|
@@ -56,7 +59,7 @@
|
|
|
56
59
|
"wrap-ansi": "^10.0.0",
|
|
57
60
|
"yargs": "^17.7.2",
|
|
58
61
|
"zod": "^3.23.8",
|
|
59
|
-
"wave-agent-sdk": "1.
|
|
62
|
+
"wave-agent-sdk": "1.4.0"
|
|
60
63
|
},
|
|
61
64
|
"engines": {
|
|
62
65
|
"node": ">=22"
|
package/src/cli.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
+
import { ensureRuntimeDeps } from "wave-agent-sdk";
|
|
3
4
|
import { App } from "./components/App.js";
|
|
4
5
|
import { cleanupLogs } from "./utils/logger.js";
|
|
5
6
|
import { removeWorktree } from "./utils/worktree.js";
|
|
@@ -11,6 +12,14 @@ export interface CliOptions extends BaseAppProps {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export async function startCli(options: CliOptions): Promise<void> {
|
|
15
|
+
// Install the on-demand runtime dependencies (image codec + ripgrep) before
|
|
16
|
+
// the UI comes up. Waiting is the point: pasting an image or running a search
|
|
17
|
+
// a second later must not race a download. Costs ~16ms once they are
|
|
18
|
+
// installed (marker hit), ~6MB/20MB worth of time on the first run of a
|
|
19
|
+
// host-bundled CLI. A failed download only degrades the affected tool — never
|
|
20
|
+
// throws.
|
|
21
|
+
await ensureRuntimeDeps();
|
|
22
|
+
|
|
14
23
|
const {
|
|
15
24
|
restoreSessionId,
|
|
16
25
|
continueLastSession,
|
|
@@ -8,17 +8,16 @@ export async function installPluginCommand(argv: {
|
|
|
8
8
|
const pluginCore = new PluginCore(workdir);
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
// When no scope is given, default to user scope (matches Claude Code).
|
|
12
|
+
const scope = argv.scope ?? "user";
|
|
13
|
+
const installed = await pluginCore.installPlugin(argv.plugin, scope);
|
|
12
14
|
console.log(
|
|
13
15
|
`Successfully installed plugin: ${installed.name} v${installed.version} from ${installed.marketplace}`,
|
|
14
16
|
);
|
|
15
17
|
console.log(`Cache path: ${installed.cachePath}`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const pluginId = `${installed.name}@${installed.marketplace}`;
|
|
20
|
-
await pluginCore.enablePlugin(pluginId, scope);
|
|
21
|
-
console.log(`Plugin ${pluginId} enabled in ${scope} scope`);
|
|
18
|
+
console.log(
|
|
19
|
+
`Plugin ${installed.name}@${installed.marketplace} enabled in ${scope} scope`,
|
|
20
|
+
);
|
|
22
21
|
|
|
23
22
|
process.exit(0);
|
|
24
23
|
} catch (error) {
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { PluginCore } from "wave-agent-sdk";
|
|
1
|
+
import { PluginCore, Scope } from "wave-agent-sdk";
|
|
2
2
|
|
|
3
|
-
export async function uninstallPluginCommand(argv: {
|
|
3
|
+
export async function uninstallPluginCommand(argv: {
|
|
4
|
+
plugin: string;
|
|
5
|
+
scope?: Scope;
|
|
6
|
+
}) {
|
|
4
7
|
const workdir = process.cwd();
|
|
5
8
|
const pluginCore = new PluginCore(workdir);
|
|
6
9
|
|
|
7
10
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
// Only the given scope is uninstalled (spec plugin A-015). Without --scope
|
|
12
|
+
// the core picks the scope the plugin is enabled in for this directory.
|
|
13
|
+
const scope = await pluginCore.uninstallPlugin(argv.plugin, argv.scope);
|
|
14
|
+
console.log(
|
|
15
|
+
`Successfully uninstalled plugin: ${argv.plugin} (scope: ${scope})`,
|
|
16
|
+
);
|
|
11
17
|
process.exit(0);
|
|
12
18
|
} catch (error) {
|
|
13
19
|
const message = error instanceof Error ? error.message : String(error);
|
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(" ")}`));
|
|
@@ -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}>
|
|
@@ -132,6 +132,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
132
132
|
toolInput={confirmingTool!.input}
|
|
133
133
|
suggestedPrefix={confirmingTool!.suggestedPrefix}
|
|
134
134
|
hidePersistentOption={confirmingTool!.hidePersistentOption}
|
|
135
|
+
outsideSafeZoneDirectory={confirmingTool!.outsideSafeZoneDirectory}
|
|
135
136
|
permissionMode={confirmingTool!.permissionMode}
|
|
136
137
|
isExpanded={isExpanded}
|
|
137
138
|
onDecision={handleConfirmationDecision}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
3
|
import {
|
|
4
4
|
BASH_TOOL_NAME,
|
|
5
5
|
EDIT_TOOL_NAME,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ARTIFACT_TOOL_NAME,
|
|
11
11
|
} from "wave-agent-sdk";
|
|
12
12
|
import { buildDiffLinesFromParams } from "./DiffDisplay.js";
|
|
13
|
+
import { renderMarkdownToAnsi } from "./Markdown.js";
|
|
14
|
+
import { ScrolledContent } from "./ScrolledContent.js";
|
|
13
15
|
import { highlightToAnsi } from "../utils/highlightUtils.js";
|
|
14
16
|
|
|
15
17
|
// Helper function to generate descriptive action text
|
|
@@ -35,7 +37,24 @@ const getActionDescription = (
|
|
|
35
37
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
36
38
|
return "Answer questions to clarify intent";
|
|
37
39
|
case ARTIFACT_TOOL_NAME:
|
|
38
|
-
|
|
40
|
+
switch (toolInput.action) {
|
|
41
|
+
case "list":
|
|
42
|
+
return `List artifacts (scope: ${toolInput.scope || "mine"})`;
|
|
43
|
+
case "upload_asset":
|
|
44
|
+
return `Upload asset: ${toolInput.file_path || "unknown file"}`;
|
|
45
|
+
case "list_assets":
|
|
46
|
+
return `List assets of artifact: ${toolInput.url || "unknown artifact"}`;
|
|
47
|
+
case "read_asset":
|
|
48
|
+
return `Read asset: ${toolInput.asset_id || "unknown asset"}`;
|
|
49
|
+
case "delete_asset":
|
|
50
|
+
return `Delete asset: ${toolInput.asset_id || "unknown asset"}`;
|
|
51
|
+
case "copy_from":
|
|
52
|
+
return `Copy assets into artifact: ${toolInput.url || "unknown artifact"}`;
|
|
53
|
+
case "read":
|
|
54
|
+
return `Read artifact: ${toolInput.url || "unknown artifact"}`;
|
|
55
|
+
default:
|
|
56
|
+
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
57
|
+
}
|
|
39
58
|
default:
|
|
40
59
|
return "Execute operation";
|
|
41
60
|
}
|
|
@@ -68,6 +87,11 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
68
87
|
(toolInput?.startLineNumber as number | undefined) ??
|
|
69
88
|
(toolName === WRITE_TOOL_NAME ? 1 : undefined);
|
|
70
89
|
|
|
90
|
+
// Terminal width is needed to render the plan's Markdown (table column widths
|
|
91
|
+
// are computed from it), matching <Markdown>'s own default.
|
|
92
|
+
const { stdout } = useStdout();
|
|
93
|
+
const columns = stdout?.columns ?? 80;
|
|
94
|
+
|
|
71
95
|
const headerRows = 2 + (warning ? 1 : 0);
|
|
72
96
|
|
|
73
97
|
// Number of content rows rendered at once. The header stays fixed above the
|
|
@@ -117,12 +141,17 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
117
141
|
toolName === EXIT_PLAN_MODE_TOOL_NAME &&
|
|
118
142
|
!!planContent;
|
|
119
143
|
|
|
144
|
+
// The plan is Markdown (the agent writes it to a .md file), so it is rendered
|
|
145
|
+
// with the same renderer as assistant messages and then linearized into rows
|
|
146
|
+
// for the row-slicing scroll above — same treatment as the JSON/diff rows.
|
|
120
147
|
const planLines = showPlan
|
|
121
|
-
? planContent
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
? renderMarkdownToAnsi(planContent!, columns)
|
|
149
|
+
.split("\n")
|
|
150
|
+
.map((line, index) => (
|
|
151
|
+
<Box key={`plan-${index}`}>
|
|
152
|
+
<Text>{line || " "}</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
))
|
|
126
155
|
: [];
|
|
127
156
|
|
|
128
157
|
const contentLines: React.ReactNode[] = [
|
|
@@ -130,9 +159,11 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
130
159
|
...jsonLines,
|
|
131
160
|
...planLines,
|
|
132
161
|
];
|
|
133
|
-
const totalLines = contentLines.length;
|
|
134
|
-
const maxScroll = Math.max(0, totalLines - visibleCount);
|
|
135
162
|
|
|
163
|
+
// Rows really occupied by the content (wrapped rows included), measured after
|
|
164
|
+
// layout by <ScrolledContent> — see that component for why it is measured
|
|
165
|
+
// instead of counted from the row array.
|
|
166
|
+
const [contentRows, setContentRows] = useState(0);
|
|
136
167
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
137
168
|
|
|
138
169
|
// Reset scrolling when a new confirmation appears.
|
|
@@ -140,6 +171,8 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
140
171
|
setScrollOffset(0);
|
|
141
172
|
}, [toolName, toolInput, planContent]);
|
|
142
173
|
|
|
174
|
+
const maxScroll = Math.max(0, contentRows - visibleCount);
|
|
175
|
+
|
|
143
176
|
const halfPage = Math.max(1, Math.ceil(visibleCount / 2));
|
|
144
177
|
useInput((input, key) => {
|
|
145
178
|
if (key.pageUp) {
|
|
@@ -159,12 +192,8 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
159
192
|
});
|
|
160
193
|
|
|
161
194
|
const clampedOffset = Math.min(scrollOffset, maxScroll);
|
|
162
|
-
const visibleLines = contentLines.slice(
|
|
163
|
-
clampedOffset,
|
|
164
|
-
clampedOffset + visibleCount,
|
|
165
|
-
);
|
|
166
195
|
const hasMoreAbove = clampedOffset > 0;
|
|
167
|
-
const hasMoreBelow = clampedOffset + visibleCount <
|
|
196
|
+
const hasMoreBelow = clampedOffset + visibleCount < contentRows;
|
|
168
197
|
|
|
169
198
|
return (
|
|
170
199
|
<Box
|
|
@@ -195,11 +224,17 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
195
224
|
</Text>
|
|
196
225
|
</Box>
|
|
197
226
|
)}
|
|
198
|
-
|
|
227
|
+
<ScrolledContent
|
|
228
|
+
visibleRows={visibleCount}
|
|
229
|
+
scrollOffset={clampedOffset}
|
|
230
|
+
onMeasuredRows={setContentRows}
|
|
231
|
+
>
|
|
232
|
+
{contentLines}
|
|
233
|
+
</ScrolledContent>
|
|
199
234
|
{hasMoreBelow && (
|
|
200
235
|
<Box>
|
|
201
236
|
<Text color="gray" dimColor>
|
|
202
|
-
↓ {
|
|
237
|
+
↓ {contentRows - clampedOffset - visibleCount} more
|
|
203
238
|
</Text>
|
|
204
239
|
</Box>
|
|
205
240
|
)}
|
|
@@ -7,8 +7,10 @@ import type {
|
|
|
7
7
|
} from "wave-agent-sdk";
|
|
8
8
|
import {
|
|
9
9
|
BASH_TOOL_NAME,
|
|
10
|
+
EDIT_TOOL_NAME,
|
|
10
11
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
11
12
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
13
|
+
WRITE_TOOL_NAME,
|
|
12
14
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
13
15
|
} from "wave-agent-sdk";
|
|
14
16
|
import { confirmationReducer } from "../reducers/confirmationReducer.js";
|
|
@@ -24,11 +26,19 @@ const getHeaderColor = (header: string) => {
|
|
|
24
26
|
return colors[Math.abs(hash) % colors.length];
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
/** Last path segment of a directory path (handles both `/` and `\` separators). */
|
|
30
|
+
const lastPathSegment = (dirPath: string): string => {
|
|
31
|
+
const segments = dirPath.split(/[\\/]/).filter(Boolean);
|
|
32
|
+
return segments[segments.length - 1] ?? dirPath;
|
|
33
|
+
};
|
|
34
|
+
|
|
27
35
|
export interface ConfirmationSelectorProps {
|
|
28
36
|
toolName: string;
|
|
29
37
|
toolInput?: Record<string, unknown>;
|
|
30
38
|
suggestedPrefix?: string;
|
|
31
39
|
hidePersistentOption?: boolean;
|
|
40
|
+
/** Directory holding an out-of-Safe-Zone Write/Edit target, when the host reports one. */
|
|
41
|
+
outsideSafeZoneDirectory?: string;
|
|
32
42
|
permissionMode?: PermissionMode;
|
|
33
43
|
isExpanded?: boolean;
|
|
34
44
|
onDecision: (decision: PermissionDecision) => void;
|
|
@@ -40,6 +50,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
40
50
|
toolInput,
|
|
41
51
|
suggestedPrefix,
|
|
42
52
|
hidePersistentOption,
|
|
53
|
+
outsideSafeZoneDirectory,
|
|
43
54
|
permissionMode,
|
|
44
55
|
isExpanded = false,
|
|
45
56
|
onDecision,
|
|
@@ -104,6 +115,15 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
104
115
|
return "Yes, and auto-accept edits";
|
|
105
116
|
};
|
|
106
117
|
|
|
118
|
+
// Only Write/Edit can be out of the Safe Zone, and the host only reports the
|
|
119
|
+
// directory for such requests.
|
|
120
|
+
const showAddDirectoryOption =
|
|
121
|
+
(toolName === EDIT_TOOL_NAME || toolName === WRITE_TOOL_NAME) &&
|
|
122
|
+
!!outsideSafeZoneDirectory;
|
|
123
|
+
const addDirectoryOptionText = `Yes, and allow all edits in ${lastPathSegment(
|
|
124
|
+
outsideSafeZoneDirectory ?? "",
|
|
125
|
+
)}/ this session`;
|
|
126
|
+
|
|
107
127
|
const pasteDetectorRef = useRef(createBracketedPasteDetector());
|
|
108
128
|
|
|
109
129
|
useInput((input, key) => {
|
|
@@ -140,6 +160,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
140
160
|
toolInput,
|
|
141
161
|
suggestedPrefix,
|
|
142
162
|
hidePersistentOption,
|
|
163
|
+
outsideSafeZoneDirectory,
|
|
143
164
|
permissionMode,
|
|
144
165
|
});
|
|
145
166
|
}
|
|
@@ -248,6 +269,24 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
248
269
|
: "Yes, proceed"}
|
|
249
270
|
</Text>
|
|
250
271
|
</Box>
|
|
272
|
+
{showAddDirectoryOption && (
|
|
273
|
+
<Box key="add-directory-option">
|
|
274
|
+
<Text
|
|
275
|
+
color={
|
|
276
|
+
state.selectedOption === "addDirectory" ? "black" : "white"
|
|
277
|
+
}
|
|
278
|
+
backgroundColor={
|
|
279
|
+
state.selectedOption === "addDirectory"
|
|
280
|
+
? "yellow"
|
|
281
|
+
: undefined
|
|
282
|
+
}
|
|
283
|
+
bold={state.selectedOption === "addDirectory"}
|
|
284
|
+
>
|
|
285
|
+
{state.selectedOption === "addDirectory" ? "> " : " "}
|
|
286
|
+
{addDirectoryOptionText}
|
|
287
|
+
</Text>
|
|
288
|
+
</Box>
|
|
289
|
+
)}
|
|
251
290
|
{!hidePersistentOption && (
|
|
252
291
|
<Box key="auto-option">
|
|
253
292
|
<Text
|