wave-code 1.1.3 → 1.1.4
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 +399 -407
- package/package.json +3 -3
- package/src/commands/update.ts +25 -8
- package/src/components/HelpView.tsx +6 -1
- package/src/index.ts +10 -2
- package/src/managers/inputHandlers.ts +2 -1
- package/src/managers/inputReducer.ts +10 -1
- package/src/stdio/agentBridge.ts +34 -1
- package/src/utils/clipboard.ts +46 -42
- package/src/utils/constants.ts +7 -2
- package/src/utils/highlightUtils.ts +6 -0
- package/src/utils/logger.ts +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"wrap-ansi": "^10.0.0",
|
|
57
57
|
"yargs": "^17.7.2",
|
|
58
58
|
"zod": "^3.23.8",
|
|
59
|
-
"wave-agent-sdk": "1.1.
|
|
59
|
+
"wave-agent-sdk": "1.1.4"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
62
62
|
"node": ">=22"
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"test:unit": "vitest run --reporter=dot --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
|
|
72
72
|
"test:unit:coverage": "vitest run --coverage --reporter=dot --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
|
|
73
73
|
"test:integration": "vitest run --reporter=dot tests/integration .integration.test",
|
|
74
|
-
"lint": "
|
|
74
|
+
"lint": "oxlint",
|
|
75
75
|
"format": "prettier --write ."
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/commands/update.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync } from "child_process";
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
2
|
import https from "https";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { isUpdateAvailable } from "../utils/version.js";
|
|
@@ -83,6 +83,25 @@ export async function updateCommand() {
|
|
|
83
83
|
console.log(chalk.blue(`Updating WAVE Code using ${packageManager}...`));
|
|
84
84
|
console.log(chalk.dim(`Running: ${updateCmd} ${args.join(" ")}`));
|
|
85
85
|
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
// On Windows the running `wave` process keeps the global bin shims
|
|
88
|
+
// (e.g. %APPDATA%\npm\wave.cmd) locked, so npm cannot overwrite them
|
|
89
|
+
// while we are still alive. Exit first and let a detached child process
|
|
90
|
+
// perform the install after a short delay.
|
|
91
|
+
console.log(
|
|
92
|
+
chalk.yellow(
|
|
93
|
+
"The update will finish in the background. Close and reopen wave afterwards.",
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
const child = spawn(
|
|
97
|
+
"cmd.exe",
|
|
98
|
+
["/c", `timeout /t 2 /nobreak >nul & ${updateCmd} ${args.join(" ")}`],
|
|
99
|
+
{ detached: true, stdio: "ignore", windowsHide: true },
|
|
100
|
+
);
|
|
101
|
+
child.unref();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
const result = spawnSync(updateCmd, args, { stdio: "inherit" });
|
|
87
106
|
|
|
88
107
|
if (result.status === 0) {
|
|
@@ -95,13 +114,11 @@ export async function updateCommand() {
|
|
|
95
114
|
`Please try running the update command manually: ${updateCmd} ${args.join(" ")}`,
|
|
96
115
|
),
|
|
97
116
|
);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
}
|
|
117
|
+
console.log(
|
|
118
|
+
chalk.yellow(
|
|
119
|
+
"You might need to run it with sudo if you encounter permission issues.",
|
|
120
|
+
),
|
|
121
|
+
);
|
|
105
122
|
process.exit(1);
|
|
106
123
|
}
|
|
107
124
|
} catch (error) {
|
|
@@ -72,7 +72,12 @@ export const HelpView: React.FC<HelpViewProps> = ({
|
|
|
72
72
|
{ key: "Ctrl+O", description: "Expand/collapse messages" },
|
|
73
73
|
{ key: "Ctrl+T", description: "Toggle task list" },
|
|
74
74
|
{ key: "Ctrl+B", description: "Background current task" },
|
|
75
|
-
|
|
75
|
+
// Windows terminals reserve Ctrl+V for system paste (it never reaches the
|
|
76
|
+
// app), so image paste is Alt+V there — same split as Claude Code.
|
|
77
|
+
{
|
|
78
|
+
key: process.platform === "win32" ? "Alt+V" : "Ctrl+V",
|
|
79
|
+
description: "Paste image",
|
|
80
|
+
},
|
|
76
81
|
{ key: "Ctrl+J", description: "Newline" },
|
|
77
82
|
{ key: "Ctrl+A", description: "Cursor to line start" },
|
|
78
83
|
{ key: "Ctrl+E", description: "Cursor to line end" },
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "wave-agent-sdk";
|
|
10
10
|
import { createWorktree, type WorktreeSession } from "./utils/worktree.js";
|
|
11
11
|
import path from "path";
|
|
12
|
+
import { pathToFileURL } from "url";
|
|
12
13
|
import { readNearestPackageJson } from "./utils/readPackageJson.js";
|
|
13
14
|
|
|
14
15
|
const version = readNearestPackageJson().version;
|
|
@@ -642,8 +643,15 @@ export {
|
|
|
642
643
|
type ClipboardImageResult,
|
|
643
644
|
} from "./utils/clipboard.js";
|
|
644
645
|
|
|
645
|
-
// Execute main function if this file is run directly
|
|
646
|
-
|
|
646
|
+
// Execute main function if this file is run directly. Compare via
|
|
647
|
+
// pathToFileURL: on Windows process.argv[1] keeps backslashes while
|
|
648
|
+
// import.meta.url is a forward-slash file:// URL, so a naive string
|
|
649
|
+
// concatenation never matches and main() silently never runs.
|
|
650
|
+
const entryFile = process.argv[1];
|
|
651
|
+
if (
|
|
652
|
+
entryFile &&
|
|
653
|
+
pathToFileURL(path.resolve(entryFile)).href === import.meta.url
|
|
654
|
+
) {
|
|
647
655
|
main().catch((error) => {
|
|
648
656
|
console.error("Failed to start WAVE Code:", error);
|
|
649
657
|
process.exit(1);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
InputState,
|
|
6
6
|
InputAction,
|
|
7
7
|
InputManagerCallbacks,
|
|
8
|
+
isPasteImageKey,
|
|
8
9
|
} from "./inputReducer.js";
|
|
9
10
|
|
|
10
11
|
export const expandLongTextPlaceholders = (
|
|
@@ -677,7 +678,7 @@ export const handleNormalInput = async (
|
|
|
677
678
|
return true;
|
|
678
679
|
}
|
|
679
680
|
|
|
680
|
-
if (key
|
|
681
|
+
if (isPasteImageKey(key, input)) {
|
|
681
682
|
handlePasteImage(dispatch).catch((error) => {
|
|
682
683
|
console.warn("Failed to handle paste image:", error);
|
|
683
684
|
});
|
|
@@ -37,6 +37,15 @@ export const btwOverlayActiveRef: { current: boolean } = { current: false };
|
|
|
37
37
|
|
|
38
38
|
export const ESC_DOUBLE_PRESS_TIMEOUT_MS = 1000;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Paste-image shortcut: Ctrl+V everywhere, plus Alt+V on Windows — Windows
|
|
42
|
+
* terminals reserve Ctrl+V for their own system paste, so the key never
|
|
43
|
+
* reaches the app (same platform split as Claude Code). Ink reports Alt+V
|
|
44
|
+
* as meta.
|
|
45
|
+
*/
|
|
46
|
+
export const isPasteImageKey = (key: Key, input: string): boolean =>
|
|
47
|
+
(key.ctrl || (process.platform === "win32" && key.meta)) && input === "v";
|
|
48
|
+
|
|
40
49
|
export type PendingEffect =
|
|
41
50
|
| {
|
|
42
51
|
type: "SEND_MESSAGE";
|
|
@@ -1121,7 +1130,7 @@ export function inputReducer(
|
|
|
1121
1130
|
};
|
|
1122
1131
|
}
|
|
1123
1132
|
|
|
1124
|
-
if (key
|
|
1133
|
+
if (isPasteImageKey(key, input)) {
|
|
1125
1134
|
return { ...state, pendingEffect: { type: "PASTE_IMAGE" } };
|
|
1126
1135
|
}
|
|
1127
1136
|
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -739,7 +739,26 @@ export class AgentBridge {
|
|
|
739
739
|
this.canUseTool(context, ctx),
|
|
740
740
|
};
|
|
741
741
|
|
|
742
|
-
|
|
742
|
+
let agent: Agent;
|
|
743
|
+
try {
|
|
744
|
+
agent = await Agent.create(options);
|
|
745
|
+
} catch (createError) {
|
|
746
|
+
// The old entry was already destroyed and removed above. If the session
|
|
747
|
+
// file is missing or unrecoverable (fresh session, cleared chat, or a
|
|
748
|
+
// never-persisted empty session), fail soft: recreate WITHOUT
|
|
749
|
+
// restoreSessionId so this session slot keeps working. Without this the
|
|
750
|
+
// client's sessionId still points at the destroyed entry and every later
|
|
751
|
+
// request fails with "Session not found" until the window is reloaded.
|
|
752
|
+
// Non-session errors (bad baseURL/apiKey/config) must surface unchanged.
|
|
753
|
+
if (!options.restoreSessionId || !isSessionRecoveryError(createError)) {
|
|
754
|
+
throw createError;
|
|
755
|
+
}
|
|
756
|
+
logger?.warn(
|
|
757
|
+
`updateConfig: failed to restore session ${currentSessionId}, recreating as a fresh session:`,
|
|
758
|
+
createError,
|
|
759
|
+
);
|
|
760
|
+
agent = await Agent.create({ ...options, restoreSessionId: undefined });
|
|
761
|
+
}
|
|
743
762
|
ctx.agent = agent;
|
|
744
763
|
ctx.registeredSessionId = agent.sessionId;
|
|
745
764
|
this.sessions.set(agent.sessionId, {
|
|
@@ -1623,3 +1642,17 @@ export class RpcError extends Error {
|
|
|
1623
1642
|
return { code: this.code, message: this.message };
|
|
1624
1643
|
}
|
|
1625
1644
|
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* True when the error means the restoreSessionId session cannot be recovered
|
|
1648
|
+
* from disk (missing file, corrupt transcript, etc.) — the recovery actions in
|
|
1649
|
+
* updateConfig/restoreSession degrade to a fresh session for these. Anything
|
|
1650
|
+
* else (config validation, plugin load, …) must surface to the client.
|
|
1651
|
+
*/
|
|
1652
|
+
function isSessionRecoveryError(error: unknown): boolean {
|
|
1653
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1654
|
+
return (
|
|
1655
|
+
message.includes("not found on disk") ||
|
|
1656
|
+
message.startsWith("Session not found:")
|
|
1657
|
+
);
|
|
1658
|
+
}
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -140,30 +140,50 @@ async function readClipboardImageMac(): Promise<ClipboardImageResult> {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Run a PowerShell script via execFile (no cmd shell), returning stdout.
|
|
145
|
+
*
|
|
146
|
+
* The script must be passed as a single argv element: routing
|
|
147
|
+
* `powershell -Command "<script>"` through exec() / cmd /c mangles the
|
|
148
|
+
* nested double quotes (cmd strips them), so PowerShell receives garbage
|
|
149
|
+
* and exits 0 with empty output — image paste silently failed on Windows.
|
|
150
|
+
*/
|
|
151
|
+
async function runPowerShellScript(script: string): Promise<string> {
|
|
152
|
+
const { execFile } = await import("child_process");
|
|
153
|
+
const { promisify } = await import("util");
|
|
154
|
+
const execFileAsync = promisify(execFile);
|
|
155
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
156
|
+
"-NoProfile",
|
|
157
|
+
"-Command",
|
|
158
|
+
script,
|
|
159
|
+
]);
|
|
160
|
+
return stdout;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check script shared by the two Windows clipboard functions. Get-Clipboard
|
|
164
|
+
// is PowerShell 5.1's built-in cmdlet (same as Claude Code): no WinForms/STA
|
|
165
|
+
// ceremony, returns a System.Drawing.Image directly.
|
|
166
|
+
const CLIPBOARD_IMAGE_CHECK_SCRIPT = `
|
|
167
|
+
$image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue
|
|
168
|
+
if ($null -ne $image) {
|
|
169
|
+
Write-Output "true"
|
|
170
|
+
} else {
|
|
171
|
+
Write-Output "false"
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
async function hasPowerShellClipboardImage(): Promise<boolean> {
|
|
176
|
+
const stdout = await runPowerShellScript(CLIPBOARD_IMAGE_CHECK_SCRIPT);
|
|
177
|
+
return stdout.trim() === "true";
|
|
178
|
+
}
|
|
179
|
+
|
|
143
180
|
/**
|
|
144
181
|
* Read clipboard image on Windows
|
|
145
182
|
*/
|
|
146
183
|
async function readClipboardImageWindows(): Promise<ClipboardImageResult> {
|
|
147
184
|
try {
|
|
148
|
-
const { exec } = await import("child_process");
|
|
149
|
-
const { promisify } = await import("util");
|
|
150
|
-
const execAsync = promisify(exec);
|
|
151
|
-
|
|
152
|
-
// Use PowerShell to check if clipboard contains image
|
|
153
|
-
const checkScript = `
|
|
154
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
155
|
-
if ([System.Windows.Forms.Clipboard]::ContainsImage()) {
|
|
156
|
-
Write-Output "true"
|
|
157
|
-
} else {
|
|
158
|
-
Write-Output "false"
|
|
159
|
-
}
|
|
160
|
-
`;
|
|
161
|
-
|
|
162
185
|
try {
|
|
163
|
-
const
|
|
164
|
-
`powershell -Command "${checkScript}"`,
|
|
165
|
-
);
|
|
166
|
-
const hasImage = stdout.trim() === "true";
|
|
186
|
+
const hasImage = await hasPowerShellClipboardImage();
|
|
167
187
|
|
|
168
188
|
if (!hasImage) {
|
|
169
189
|
return {
|
|
@@ -175,22 +195,20 @@ async function readClipboardImageWindows(): Promise<ClipboardImageResult> {
|
|
|
175
195
|
// Generate temporary file path
|
|
176
196
|
const tempFilePath = join(tmpdir(), `clipboard-image-${Date.now()}.png`);
|
|
177
197
|
|
|
178
|
-
//
|
|
198
|
+
// Save the clipboard image via PowerShell (single-quoted path; a single
|
|
199
|
+
// quote in the path is escaped by doubling it). Keeps its own preamble
|
|
200
|
+
// because it needs the $image variable.
|
|
179
201
|
const saveScript = `
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if ($image -ne $null) {
|
|
184
|
-
$image.Save("${tempFilePath.replace(/\\/g, "\\\\")}", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
202
|
+
$image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue
|
|
203
|
+
if ($null -ne $image) {
|
|
204
|
+
$image.Save('${tempFilePath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
185
205
|
Write-Output "true"
|
|
186
206
|
} else {
|
|
187
207
|
Write-Output "false"
|
|
188
208
|
}
|
|
189
209
|
`;
|
|
190
210
|
|
|
191
|
-
const
|
|
192
|
-
`powershell -Command "${saveScript}"`,
|
|
193
|
-
);
|
|
211
|
+
const saveResult = await runPowerShellScript(saveScript);
|
|
194
212
|
|
|
195
213
|
if (saveResult.trim() !== "true" || !existsSync(tempFilePath)) {
|
|
196
214
|
return {
|
|
@@ -354,21 +372,7 @@ async function hasClipboardImageMac(): Promise<boolean> {
|
|
|
354
372
|
*/
|
|
355
373
|
async function hasClipboardImageWindows(): Promise<boolean> {
|
|
356
374
|
try {
|
|
357
|
-
|
|
358
|
-
const { promisify } = await import("util");
|
|
359
|
-
const execAsync = promisify(exec);
|
|
360
|
-
|
|
361
|
-
const checkScript = `
|
|
362
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
363
|
-
if ([System.Windows.Forms.Clipboard]::ContainsImage()) {
|
|
364
|
-
Write-Output "true"
|
|
365
|
-
} else {
|
|
366
|
-
Write-Output "false"
|
|
367
|
-
}
|
|
368
|
-
`;
|
|
369
|
-
|
|
370
|
-
const { stdout } = await execAsync(`powershell -Command "${checkScript}"`);
|
|
371
|
-
return stdout.trim() === "true";
|
|
375
|
+
return await hasPowerShellClipboardImage();
|
|
372
376
|
} catch {
|
|
373
377
|
return false;
|
|
374
378
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -12,9 +12,14 @@ import os from "os";
|
|
|
12
12
|
export const DATA_DIRECTORY = path.join(os.homedir(), ".wave");
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Log directory — one file per surface (cli/desktop/vscode/jetbrains).
|
|
16
16
|
*/
|
|
17
|
-
export const
|
|
17
|
+
export const LOGS_DIRECTORY = path.join(DATA_DIRECTORY, "logs");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* CLI log file path
|
|
21
|
+
*/
|
|
22
|
+
export const LOG_FILE = path.join(LOGS_DIRECTORY, "cli.log");
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Pagination related constants
|
|
@@ -108,6 +108,12 @@ export function highlightToAnsi(code: string, language?: string): string {
|
|
|
108
108
|
if (!code) {
|
|
109
109
|
return "";
|
|
110
110
|
}
|
|
111
|
+
// hljs.highlight logs a console.error (then throws) when the language is not
|
|
112
|
+
// registered. We bundle a trimmed language set, so check first and fall back
|
|
113
|
+
// to plain text instead of spamming stderr with LANGUAGE_NOT_FOUND noise.
|
|
114
|
+
if (language && !hljs.getLanguage(language)) {
|
|
115
|
+
return code;
|
|
116
|
+
}
|
|
111
117
|
try {
|
|
112
118
|
const highlighted = language
|
|
113
119
|
? hljs.highlight(code, { language }).value
|
package/src/utils/logger.ts
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
12
13
|
import { Chalk } from "chalk";
|
|
13
14
|
import { getLastLines } from "wave-agent-sdk";
|
|
14
|
-
import { LOG_FILE
|
|
15
|
+
import { LOG_FILE } from "./constants.js";
|
|
15
16
|
|
|
16
17
|
const chalk = new Chalk({ level: 3 });
|
|
17
18
|
|
|
@@ -166,9 +167,10 @@ const logMessage = (level: LogLevel, ...args: unknown[]): void => {
|
|
|
166
167
|
const formattedMessage = `[${chalk.gray(timestamp)}] [${color(levelName)}] ${messageText}\n`;
|
|
167
168
|
|
|
168
169
|
try {
|
|
169
|
-
// Ensure directory exists
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
// Ensure the log file's directory exists (LOG_FILE moved to ~/.wave/logs)
|
|
171
|
+
const logDir = path.dirname(logFile);
|
|
172
|
+
if (!fs.existsSync(logDir)) {
|
|
173
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
// Write log to file
|