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.
- package/dist/contexts/useChat.js +14 -13
- package/dist/stdio/jsonRpcConnection.d.ts +10 -1
- package/dist/stdio/jsonRpcConnection.js +9 -1
- package/dist/stdio/stdioServer.d.ts +8 -0
- package/dist/stdio/stdioServer.js +16 -1
- package/package.json +2 -2
- package/src/contexts/useChat.tsx +16 -15
- package/src/stdio/jsonRpcConnection.ts +19 -1
- package/src/stdio/stdioServer.ts +17 -0
package/dist/contexts/useChat.js
CHANGED
|
@@ -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
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
46
|
+
"wave-agent-sdk": "1.0.10"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.1.8",
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -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
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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;
|
package/src/stdio/stdioServer.ts
CHANGED
|
@@ -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
|
}
|