wave-code 1.0.9 → 1.0.10

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.
@@ -407,19 +407,20 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
407
407
  if (isExpandedRef.current)
408
408
  return;
409
409
  setMessages((prev) => {
410
- // Append to the last assistant message, or create one if none exists
411
- for (let i = prev.length - 1; i >= 0; i--) {
412
- if (prev[i].role === "assistant") {
413
- return prev.map((m, idx) => idx === i
414
- ? {
415
- ...m,
416
- blocks: [
417
- ...m.blocks,
418
- { type: "error", content: error },
419
- ],
420
- }
421
- : m);
422
- }
410
+ // Append to the LAST message only if it is an assistant message
411
+ // (the current turn's in-flight reply). If the last message is a
412
+ // user message, the error belongs BELOW it — create a new
413
+ // assistant message instead of polluting the stale assistant from
414
+ // an earlier turn (which surfaced the error above the latest
415
+ // user message and accumulated it there).
416
+ const last = prev[prev.length - 1];
417
+ if (last && last.role === "assistant") {
418
+ return prev.map((m, idx) => idx === prev.length - 1
419
+ ? {
420
+ ...m,
421
+ blocks: [...m.blocks, { type: "error", content: error }],
422
+ }
423
+ : m);
423
424
  }
424
425
  return [
425
426
  ...prev,
@@ -9,13 +9,22 @@
9
9
  */
10
10
  import type { Readable, Writable } from "stream";
11
11
  import { AgentBridge } from "./agentBridge.js";
12
+ export interface JsonRpcConnectionOptions {
13
+ /** Called when the input stream reaches EOF (parent closed the pipe). Only
14
+ * stdio mode passes it — it must exit the process when its client dies;
15
+ * daemon mode never passes it (a detached client must not kill the daemon,
16
+ * see daemonServer.ts). */
17
+ onClose?: () => void;
18
+ }
12
19
  export declare class JsonRpcConnection {
13
20
  private input;
14
21
  private output;
15
22
  private bridge;
16
23
  private rl;
17
24
  private started;
18
- constructor(input: Readable, output: Writable, bridge: AgentBridge);
25
+ private stoppedByOwner;
26
+ private onClose?;
27
+ constructor(input: Readable, output: Writable, bridge: AgentBridge, options?: JsonRpcConnectionOptions);
19
28
  start(): void;
20
29
  stop(): void;
21
30
  handleLine(line: string): Promise<void>;
@@ -10,11 +10,13 @@
10
10
  import readline from "readline";
11
11
  import { PARSE_ERROR, INVALID_REQUEST, INTERNAL_ERROR, isRequest, isNotification, } from "./protocol.js";
12
12
  export class JsonRpcConnection {
13
- constructor(input, output, bridge) {
13
+ constructor(input, output, bridge, options = {}) {
14
14
  this.input = input;
15
15
  this.output = output;
16
16
  this.bridge = bridge;
17
17
  this.started = false;
18
+ this.stoppedByOwner = false;
19
+ this.onClose = options.onClose;
18
20
  }
19
21
  start() {
20
22
  if (this.started)
@@ -39,9 +41,15 @@ export class JsonRpcConnection {
39
41
  });
40
42
  this.rl.on("close", () => {
41
43
  this.started = false;
44
+ // stdin EOF from the owning client — not a deliberate stop(). stdio
45
+ // mode reacts by exiting the process (its parent is gone); daemon mode
46
+ // has no onClose so a detached client reset never kills the daemon.
47
+ if (!this.stoppedByOwner)
48
+ this.onClose?.();
42
49
  });
43
50
  }
44
51
  stop() {
52
+ this.stoppedByOwner = true;
45
53
  this.rl?.close();
46
54
  this.rl = undefined;
47
55
  this.started = false;
@@ -19,6 +19,14 @@ export declare class StdioServer {
19
19
  get agentBridge(): AgentBridge;
20
20
  start(): void;
21
21
  stop(): void;
22
+ /**
23
+ * The owning client closed stdin (EOF) — it's gone, so this process must
24
+ * follow it. destroyAll() saves each session's transcript and drains
25
+ * auto-memory first; a timeout bounds it so a stuck agent can't orphan the
26
+ * exit. This is the real-world "parent died" path: without it the process
27
+ * lingers forever whenever an agent keeps event-loop handles alive.
28
+ */
29
+ private onConnectionClosed;
22
30
  handleLine(line: string): Promise<void>;
23
31
  sendResponse(id: number | string | null, result?: unknown, error?: {
24
32
  code: number;
@@ -13,7 +13,7 @@ export class StdioServer {
13
13
  ...options.bridgeOptions,
14
14
  emit: (method, params, sessionId) => this.sendNotification(method, params, sessionId),
15
15
  });
16
- this.conn = new JsonRpcConnection(options.input ?? process.stdin, options.output ?? process.stdout, this.bridge);
16
+ this.conn = new JsonRpcConnection(options.input ?? process.stdin, options.output ?? process.stdout, this.bridge, { onClose: () => this.onConnectionClosed() });
17
17
  }
18
18
  get agentBridge() {
19
19
  return this.bridge;
@@ -24,6 +24,21 @@ export class StdioServer {
24
24
  stop() {
25
25
  this.conn.stop();
26
26
  }
27
+ /**
28
+ * The owning client closed stdin (EOF) — it's gone, so this process must
29
+ * follow it. destroyAll() saves each session's transcript and drains
30
+ * auto-memory first; a timeout bounds it so a stuck agent can't orphan the
31
+ * exit. This is the real-world "parent died" path: without it the process
32
+ * lingers forever whenever an agent keeps event-loop handles alive.
33
+ */
34
+ async onConnectionClosed() {
35
+ const SHUTDOWN_TIMEOUT_MS = 5000;
36
+ await Promise.race([
37
+ this.bridge.destroyAll(),
38
+ new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
39
+ ]);
40
+ process.exit(0);
41
+ }
27
42
  handleLine(line) {
28
43
  return this.conn.handleLine(line);
29
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "wrap-ansi": "^10.0.0",
44
44
  "yargs": "^17.7.2",
45
45
  "zod": "^3.23.8",
46
- "wave-agent-sdk": "1.0.9"
46
+ "wave-agent-sdk": "1.0.10"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/react": "^19.1.8",
@@ -703,21 +703,22 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
703
703
  onErrorBlockAdded: (error: string) => {
704
704
  if (isExpandedRef.current) return;
705
705
  setMessages((prev) => {
706
- // Append to the last assistant message, or create one if none exists
707
- for (let i = prev.length - 1; i >= 0; i--) {
708
- if (prev[i].role === "assistant") {
709
- return prev.map((m, idx) =>
710
- idx === i
711
- ? {
712
- ...m,
713
- blocks: [
714
- ...m.blocks,
715
- { type: "error", content: error },
716
- ],
717
- }
718
- : m,
719
- );
720
- }
706
+ // Append to the LAST message only if it is an assistant message
707
+ // (the current turn's in-flight reply). If the last message is a
708
+ // user message, the error belongs BELOW it — create a new
709
+ // assistant message instead of polluting the stale assistant from
710
+ // an earlier turn (which surfaced the error above the latest
711
+ // user message and accumulated it there).
712
+ const last = prev[prev.length - 1];
713
+ if (last && last.role === "assistant") {
714
+ return prev.map((m, idx) =>
715
+ idx === prev.length - 1
716
+ ? {
717
+ ...m,
718
+ blocks: [...m.blocks, { type: "error", content: error }],
719
+ }
720
+ : m,
721
+ );
721
722
  }
722
723
  return [
723
724
  ...prev,
@@ -22,15 +22,28 @@ import {
22
22
  isNotification,
23
23
  } from "./protocol.js";
24
24
 
25
+ export interface JsonRpcConnectionOptions {
26
+ /** Called when the input stream reaches EOF (parent closed the pipe). Only
27
+ * stdio mode passes it — it must exit the process when its client dies;
28
+ * daemon mode never passes it (a detached client must not kill the daemon,
29
+ * see daemonServer.ts). */
30
+ onClose?: () => void;
31
+ }
32
+
25
33
  export class JsonRpcConnection {
26
34
  private rl: readline.Interface | undefined;
27
35
  private started = false;
36
+ private stoppedByOwner = false;
37
+ private onClose?: () => void;
28
38
 
29
39
  constructor(
30
40
  private input: Readable,
31
41
  private output: Writable,
32
42
  private bridge: AgentBridge,
33
- ) {}
43
+ options: JsonRpcConnectionOptions = {},
44
+ ) {
45
+ this.onClose = options.onClose;
46
+ }
34
47
 
35
48
  start(): void {
36
49
  if (this.started) return;
@@ -58,10 +71,15 @@ export class JsonRpcConnection {
58
71
 
59
72
  this.rl.on("close", () => {
60
73
  this.started = false;
74
+ // stdin EOF from the owning client — not a deliberate stop(). stdio
75
+ // mode reacts by exiting the process (its parent is gone); daemon mode
76
+ // has no onClose so a detached client reset never kills the daemon.
77
+ if (!this.stoppedByOwner) this.onClose?.();
61
78
  });
62
79
  }
63
80
 
64
81
  stop(): void {
82
+ this.stoppedByOwner = true;
65
83
  this.rl?.close();
66
84
  this.rl = undefined;
67
85
  this.started = false;
@@ -30,6 +30,7 @@ export class StdioServer {
30
30
  options.input ?? process.stdin,
31
31
  options.output ?? process.stdout,
32
32
  this.bridge,
33
+ { onClose: () => this.onConnectionClosed() },
33
34
  );
34
35
  }
35
36
 
@@ -45,6 +46,22 @@ export class StdioServer {
45
46
  this.conn.stop();
46
47
  }
47
48
 
49
+ /**
50
+ * The owning client closed stdin (EOF) — it's gone, so this process must
51
+ * follow it. destroyAll() saves each session's transcript and drains
52
+ * auto-memory first; a timeout bounds it so a stuck agent can't orphan the
53
+ * exit. This is the real-world "parent died" path: without it the process
54
+ * lingers forever whenever an agent keeps event-loop handles alive.
55
+ */
56
+ private async onConnectionClosed(): Promise<void> {
57
+ const SHUTDOWN_TIMEOUT_MS = 5_000;
58
+ await Promise.race([
59
+ this.bridge.destroyAll(),
60
+ new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
61
+ ]);
62
+ process.exit(0);
63
+ }
64
+
48
65
  handleLine(line: string): Promise<void> {
49
66
  return this.conn.handleLine(line);
50
67
  }