wave-code 1.5.1 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,7 +59,7 @@
59
59
  "wrap-ansi": "^10.0.0",
60
60
  "yargs": "^17.7.2",
61
61
  "zod": "^3.23.8",
62
- "wave-agent-sdk": "1.5.1"
62
+ "wave-agent-sdk": "1.6.0"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=22"
@@ -69,11 +69,11 @@
69
69
  "wave": "tsx --tsconfig tsconfig.dev.json src/index.ts",
70
70
  "build": "rimraf dist && node scripts/bundle.mjs",
71
71
  "type-check": "tsc --noEmit --incremental",
72
- "test": "vitest run --reporter=dot",
73
- "test:coverage": "vitest run --coverage --reporter=dot",
74
- "test:unit": "vitest run --reporter=dot --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
75
- "test:unit:coverage": "vitest run --coverage --reporter=dot --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
76
- "test:integration": "vitest run --reporter=dot tests/integration .integration.test",
72
+ "test": "vitest run",
73
+ "test:coverage": "vitest run --coverage",
74
+ "test:unit": "vitest run --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
75
+ "test:unit:coverage": "vitest run --coverage --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
76
+ "test:integration": "vitest run tests/integration .integration.test",
77
77
  "lint": "oxlint",
78
78
  "format": "prettier --write ."
79
79
  }
package/src/cli.tsx CHANGED
@@ -3,7 +3,7 @@ import { render } from "ink";
3
3
  import { ensureRuntimeDeps } from "wave-agent-sdk";
4
4
  import { App } from "./components/App.js";
5
5
  import { cleanupLogs } from "./utils/logger.js";
6
- import { removeWorktree } from "./utils/worktree.js";
6
+ import { describeWorktreeSurvival, removeWorktree } from "./utils/worktree.js";
7
7
  import { BaseAppProps } from "./types.js";
8
8
 
9
9
  export interface CliOptions extends BaseAppProps {
@@ -96,8 +96,14 @@ export async function startCli(options: CliOptions): Promise<void> {
96
96
  if (shouldRemoveWorktree && worktreeSession) {
97
97
  process.stdout.write("\nDeleting worktree ...\n");
98
98
  process.chdir(worktreeSession.repoRoot);
99
- await removeWorktree(worktreeSession);
100
- process.stdout.write("Done.\n");
99
+ // "Done." only when it is: removal is best-effort, and a surviving
100
+ // directory (with its branch kept for it) needs the cleanup commands
101
+ // printed rather than a success line over the leftovers.
102
+ process.stdout.write(
103
+ (await removeWorktree(worktreeSession))
104
+ ? "Done.\n"
105
+ : `\n${describeWorktreeSurvival(worktreeSession)}\n`,
106
+ );
101
107
  }
102
108
 
103
109
  process.exit(0);
@@ -157,6 +157,26 @@ export const McpManager: React.FC<McpManagerProps> = ({
157
157
  </Text>
158
158
  </Box>
159
159
  )}
160
+ {/* Not dialed yet (lazy dial): the tool list comes from the local
161
+ snapshot of the last successful connection, and says so. */}
162
+ {selectedServer.status !== "connected" &&
163
+ selectedServer.toolsFromCache && (
164
+ <Box>
165
+ <Text>
166
+ <Text color="blue">Tools:</Text>{" "}
167
+ {selectedServer.toolsFromCache.length} tools{" "}
168
+ <Text color="gray" dimColor>
169
+ (cached
170
+ {selectedServer.toolsCachedAt
171
+ ? ` · last connected ${formatTime(
172
+ selectedServer.toolsCachedAt,
173
+ )}`
174
+ : ""}
175
+ )
176
+ </Text>
177
+ </Text>
178
+ </Box>
179
+ )}
160
180
  {selectedServer.capabilities && (
161
181
  <Box>
162
182
  <Text>
@@ -270,6 +290,12 @@ export const McpManager: React.FC<McpManagerProps> = ({
270
290
  {server.status === "connected" && server.toolCount && (
271
291
  <Text color="green"> · {server.toolCount} tools</Text>
272
292
  )}
293
+ {server.status !== "connected" && server.toolsFromCache && (
294
+ <Text color="gray" dimColor>
295
+ {" "}
296
+ · {server.toolsFromCache.length} tools (cached)
297
+ </Text>
298
+ )}
273
299
  </Text>
274
300
  {index === state.selectedIndex && (
275
301
  <Box marginLeft={4} flexDirection="column">
@@ -41,9 +41,21 @@ export const MessageBlockItem = ({
41
41
  </Box>
42
42
  )}
43
43
 
44
- {block.type === "error" && (
44
+ {/* A line for the human, never sent to the model (see NoticeBlock in the
45
+ SDK): a failed turn (`error`, red) or a hook's status line (`info`,
46
+ muted). */}
47
+ {block.type === "notice" && (
45
48
  <Box>
46
- <Text color="red">Error: {block.content}</Text>
49
+ {block.level === "error" ? (
50
+ <Text color="red">Error: {block.content}</Text>
51
+ ) : (
52
+ <Text
53
+ color={block.level === "warning" ? "yellow" : "gray"}
54
+ dimColor
55
+ >
56
+ {block.content}
57
+ </Text>
58
+ )}
47
59
  </Box>
48
60
  )}
49
61
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * RemoteControlView — `/remote-control` 的覆盖层(把这条前台会话交给手机)。
3
3
  *
4
- * 规格:docs/specs/ui/remote-control.md「前台 CLI 会话也能被手机接管」。
4
+ * 规格:docs/specs/remote/remote-web.md「前台 CLI 会话也能被手机接管」。
5
5
  *
6
6
  * **这个斜杠命令不带参数**:没有 `on` / `off` / `status` 子命令,也不收机器名。
7
7
  * 判据是「状态自己决定动作」——`off` / `error` 时唯一合法动作是开启,`connecting`
@@ -14,6 +14,7 @@ import {
14
14
  } from "../hooks/useRemoteControl.js";
15
15
  import type {
16
16
  Message,
17
+ MessageBlock,
17
18
  McpServerStatus,
18
19
  BackgroundTask,
19
20
  Task,
@@ -363,6 +364,38 @@ const applyToolBlockUpdate = (
363
364
  });
364
365
  };
365
366
 
367
+ /**
368
+ * Append a block to the last message — only when it is an assistant message
369
+ * (the current turn's in-flight reply). If the last message is a user message,
370
+ * the block belongs BELOW it: create a new assistant message instead of
371
+ * polluting the stale assistant from an earlier turn. Same rule the webview
372
+ * reducer applies (APPEND_NOTICE_BLOCK).
373
+ *
374
+ * Both the error and the system line use this: the SDK appends them to the
375
+ * same place (`appendBlockToLastAssistant`), so the CLI must not invent a
376
+ * second placement rule.
377
+ */
378
+ const appendBlockToLastAssistant = (
379
+ prev: Message[],
380
+ block: MessageBlock,
381
+ ): Message[] => {
382
+ const last = prev[prev.length - 1];
383
+ if (last && last.role === "assistant") {
384
+ return prev.map((m, idx) =>
385
+ idx === prev.length - 1 ? { ...m, blocks: [...m.blocks, block] } : m,
386
+ );
387
+ }
388
+ return [
389
+ ...prev,
390
+ {
391
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
392
+ role: "assistant",
393
+ timestamp: new Date().toISOString(),
394
+ blocks: [block],
395
+ },
396
+ ];
397
+ };
398
+
366
399
  export const ChatProvider: React.FC<ChatProviderProps> = ({
367
400
  children,
368
401
  bypassPermissions,
@@ -726,39 +759,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
726
759
  // `end` carries the authoritative final values.
727
760
  setMessages((prev) => applyToolBlockUpdate(prev, params));
728
761
  },
729
- onErrorBlockAdded: (error: string) => {
730
- remoteStream?.onErrorBlockAdded(error);
762
+ onNoticeAdded: ({ level, content }) => {
763
+ remoteStream?.onNoticeAdded({ level, content });
731
764
  if (isExpandedRef.current) return;
732
- // One-shot structural update — applied directly (errors must not be
733
- // delayed by the tool-running window).
734
- setMessages((prev) => {
735
- // Append to the LAST message only if it is an assistant message
736
- // (the current turn's in-flight reply). If the last message is a
737
- // user message, the error belongs BELOW it — create a new
738
- // assistant message instead of polluting the stale assistant from
739
- // an earlier turn (which surfaced the error above the latest
740
- // user message and accumulated it there).
741
- const last = prev[prev.length - 1];
742
- if (last && last.role === "assistant") {
743
- return prev.map((m, idx) =>
744
- idx === prev.length - 1
745
- ? {
746
- ...m,
747
- blocks: [...m.blocks, { type: "error", content: error }],
748
- }
749
- : m,
750
- );
751
- }
752
- return [
753
- ...prev,
754
- {
755
- id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
756
- role: "assistant",
757
- timestamp: new Date().toISOString(),
758
- blocks: [{ type: "error", content: error }],
759
- },
760
- ];
761
- });
765
+ // One-shot structural update — applied directly (a failure must not
766
+ // be delayed by the tool-running window; a hook's round line must
767
+ // not wait for the next `refreshMessages`).
768
+ setMessages((prev) =>
769
+ appendBlockToLastAssistant(prev, {
770
+ type: "notice",
771
+ level,
772
+ content,
773
+ }),
774
+ );
762
775
  },
763
776
  onLatestTotalTokensChange: (tokens) => {
764
777
  setLatestTotalTokens(tokens);
@@ -1213,7 +1226,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1213
1226
  timestamp: new Date().toISOString(),
1214
1227
  blocks: [
1215
1228
  {
1216
- type: "error",
1229
+ type: "notice",
1230
+ level: "error",
1217
1231
  content: `Failed to resume conversation: ${
1218
1232
  error instanceof Error ? error.message : String(error)
1219
1233
  }`,