wave-code 1.1.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.1.2",
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.2"
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": "eslint --cache",
74
+ "lint": "oxlint",
75
75
  "format": "prettier --write ."
76
76
  }
77
77
  }
@@ -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
- if (process.platform !== "win32") {
99
- console.log(
100
- chalk.yellow(
101
- "You might need to run it with sudo if you encounter permission issues.",
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
- { key: "Ctrl+V", description: "Paste image" },
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" },
@@ -7,7 +7,6 @@ import { ToolDisplay } from "./ToolDisplay.js";
7
7
  import { CompactDisplay } from "./CompactDisplay.js";
8
8
  import { ReasoningDisplay } from "./ReasoningDisplay.js";
9
9
  import { Markdown } from "./Markdown.js";
10
- import { TaskNotificationMessage } from "./TaskNotificationMessage.js";
11
10
  import { streamingTail } from "../utils/streamingText.js";
12
11
 
13
12
  export interface MessageBlockItemProps {
@@ -84,10 +83,6 @@ export const MessageBlockItem = ({
84
83
  {block.type === "reasoning" && (
85
84
  <ReasoningDisplay block={block} isExpanded={isExpanded} />
86
85
  )}
87
-
88
- {block.type === "task_notification" && (
89
- <TaskNotificationMessage block={block} />
90
- )}
91
86
  </Box>
92
87
  );
93
88
  };
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
- if (import.meta.url === `file://${process.argv[1]}`) {
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.ctrl && input === "v") {
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.ctrl && input === "v") {
1133
+ if (isPasteImageKey(key, input)) {
1125
1134
  return { ...state, pendingEffect: { type: "PASTE_IMAGE" } };
1126
1135
  }
1127
1136
 
@@ -739,7 +739,26 @@ export class AgentBridge {
739
739
  this.canUseTool(context, ctx),
740
740
  };
741
741
 
742
- const agent = await Agent.create(options);
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, {
@@ -1570,6 +1589,13 @@ export class AgentBridge {
1570
1589
  ctx.registeredSessionId,
1571
1590
  );
1572
1591
  },
1592
+ onCompactionContentUpdate: (content: string) => {
1593
+ this.emit(
1594
+ "compactionContentUpdate",
1595
+ { content },
1596
+ ctx.registeredSessionId,
1597
+ );
1598
+ },
1573
1599
  };
1574
1600
  }
1575
1601
 
@@ -1616,3 +1642,17 @@ export class RpcError extends Error {
1616
1642
  return { code: this.code, message: this.message };
1617
1643
  }
1618
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
+ }
@@ -137,6 +137,7 @@ export type ServerNotificationMethod =
137
137
  | "authUrl"
138
138
  | "compactBlockAdded"
139
139
  | "compactionStateChange"
140
+ | "compactionContentUpdate"
140
141
  | "backgroundTasksChange"
141
142
  | "btwContent";
142
143
 
@@ -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 { stdout } = await execAsync(
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
- // Use PowerShell to save clipboard image
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
- Add-Type -AssemblyName System.Windows.Forms
181
- Add-Type -AssemblyName System.Drawing
182
- $image = [System.Windows.Forms.Clipboard]::GetImage()
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 { stdout: saveResult } = await execAsync(
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
- const { exec } = await import("child_process");
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
  }
@@ -12,9 +12,14 @@ import os from "os";
12
12
  export const DATA_DIRECTORY = path.join(os.homedir(), ".wave");
13
13
 
14
14
  /**
15
- * Application log file path
15
+ * Log directory — one file per surface (cli/desktop/vscode/jetbrains).
16
16
  */
17
- export const LOG_FILE = path.join(DATA_DIRECTORY, "app.log");
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
@@ -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, DATA_DIRECTORY } from "./constants.js";
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
- if (!fs.existsSync(DATA_DIRECTORY)) {
171
- fs.mkdirSync(DATA_DIRECTORY, { recursive: true });
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
@@ -1,29 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import type { TaskNotificationBlock } from "wave-agent-sdk";
4
-
5
- export interface TaskNotificationMessageProps {
6
- block: TaskNotificationBlock;
7
- }
8
-
9
- const statusColor: Record<TaskNotificationBlock["status"], string> = {
10
- completed: "green",
11
- failed: "red",
12
- killed: "yellow",
13
- aborted: "gray",
14
- };
15
-
16
- export const TaskNotificationMessage = ({
17
- block,
18
- }: TaskNotificationMessageProps) => {
19
- const color = statusColor[block.status];
20
-
21
- return (
22
- <Box flexDirection="column">
23
- <Box>
24
- <Text color={color}>◆ </Text>
25
- <Text color="white">{block.summary}</Text>
26
- </Box>
27
- </Box>
28
- );
29
- };