wave-code 1.0.0 → 1.0.2

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.
Files changed (72) hide show
  1. package/dist/cli.js +20 -1
  2. package/dist/components/App.js +7 -0
  3. package/dist/components/BtwDisplay.js +13 -3
  4. package/dist/components/ChatInterface.js +25 -8
  5. package/dist/components/InputBox.d.ts +1 -3
  6. package/dist/components/InputBox.js +12 -9
  7. package/dist/components/LoadingIndicator.d.ts +1 -2
  8. package/dist/components/LoadingIndicator.js +2 -2
  9. package/dist/components/LoginCommand.js +4 -2
  10. package/dist/components/Markdown.js +13 -16
  11. package/dist/components/Notifications.d.ts +7 -0
  12. package/dist/components/Notifications.js +9 -0
  13. package/dist/components/StatusLine.d.ts +0 -4
  14. package/dist/components/StatusLine.js +6 -10
  15. package/dist/components/TaskList.js +2 -1
  16. package/dist/components/ToolDisplay.d.ts +1 -0
  17. package/dist/components/ToolDisplay.js +17 -9
  18. package/dist/constants/commands.js +0 -6
  19. package/dist/contexts/useChat.d.ts +4 -6
  20. package/dist/contexts/useChat.js +253 -110
  21. package/dist/daemon-cli.d.ts +10 -0
  22. package/dist/daemon-cli.js +15 -0
  23. package/dist/hooks/useInputManager.js +99 -22
  24. package/dist/index.js +10 -0
  25. package/dist/managers/inputHandlers.js +50 -22
  26. package/dist/managers/inputReducer.d.ts +12 -2
  27. package/dist/managers/inputReducer.js +57 -9
  28. package/dist/stdio/agentBridge.d.ts +23 -0
  29. package/dist/stdio/agentBridge.js +134 -16
  30. package/dist/stdio/daemonServer.d.ts +67 -0
  31. package/dist/stdio/daemonServer.js +191 -0
  32. package/dist/stdio/index.d.ts +2 -0
  33. package/dist/stdio/index.js +2 -0
  34. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  35. package/dist/stdio/jsonRpcConnection.js +127 -0
  36. package/dist/stdio/protocol.d.ts +2 -2
  37. package/dist/stdio/stdioServer.d.ts +2 -7
  38. package/dist/stdio/stdioServer.js +9 -100
  39. package/dist/utils/bracketedPaste.d.ts +39 -0
  40. package/dist/utils/bracketedPaste.js +122 -0
  41. package/dist/utils/markdownTable.d.ts +34 -0
  42. package/dist/utils/markdownTable.js +302 -0
  43. package/dist/utils/throttle.d.ts +3 -3
  44. package/package.json +4 -2
  45. package/src/cli.tsx +20 -1
  46. package/src/components/App.tsx +5 -0
  47. package/src/components/BtwDisplay.tsx +36 -12
  48. package/src/components/ChatInterface.tsx +30 -15
  49. package/src/components/InputBox.tsx +25 -24
  50. package/src/components/LoadingIndicator.tsx +1 -4
  51. package/src/components/LoginCommand.tsx +4 -2
  52. package/src/components/Markdown.tsx +15 -18
  53. package/src/components/Notifications.tsx +31 -0
  54. package/src/components/StatusLine.tsx +17 -44
  55. package/src/components/TaskList.tsx +2 -1
  56. package/src/components/ToolDisplay.tsx +17 -6
  57. package/src/constants/commands.ts +0 -6
  58. package/src/contexts/useChat.tsx +326 -140
  59. package/src/daemon-cli.ts +17 -0
  60. package/src/hooks/useInputManager.ts +108 -22
  61. package/src/index.ts +12 -0
  62. package/src/managers/inputHandlers.ts +49 -22
  63. package/src/managers/inputReducer.ts +66 -11
  64. package/src/stdio/agentBridge.ts +196 -17
  65. package/src/stdio/daemonServer.ts +212 -0
  66. package/src/stdio/index.ts +2 -0
  67. package/src/stdio/jsonRpcConnection.ts +160 -0
  68. package/src/stdio/protocol.ts +5 -2
  69. package/src/stdio/stdioServer.ts +14 -120
  70. package/src/utils/bracketedPaste.ts +170 -0
  71. package/src/utils/markdownTable.ts +359 -0
  72. package/src/utils/throttle.ts +8 -8
@@ -54,6 +54,7 @@ export type RequestMethod =
54
54
  | "getSessionInfo"
55
55
  | "sendMessage"
56
56
  | "bang"
57
+ | "askBtw"
57
58
  | "abortMessage"
58
59
  | "clearMessages"
59
60
  | "rewindToMessage"
@@ -73,6 +74,8 @@ export type RequestMethod =
73
74
  | "getPromptHistory"
74
75
  | "searchPromptHistory"
75
76
  | "updateConfig"
77
+ // Permissions (daemon attach: re-surface pending approvals after reconnect)
78
+ | "listPendingPermissions"
76
79
  // Auth
77
80
  | "getAuthStatus"
78
81
  | "login"
@@ -105,7 +108,6 @@ export type ClientNotificationMethod = "permissionResponse";
105
108
  // ── Server → Client notification methods ────────────────────────
106
109
 
107
110
  export type ServerNotificationMethod =
108
- | "messagesChange"
109
111
  | "userMessageAdded"
110
112
  | "assistantMessageAdded"
111
113
  | "assistantContentUpdated"
@@ -128,7 +130,8 @@ export type ServerNotificationMethod =
128
130
  | "authUrl"
129
131
  | "compactBlockAdded"
130
132
  | "compactionStateChange"
131
- | "backgroundTasksChange";
133
+ | "backgroundTasksChange"
134
+ | "btwContent";
132
135
 
133
136
  // ── Helper: is this a request (has id)? ─────────────────────────
134
137
 
@@ -3,21 +3,12 @@
3
3
  * and writes responses / notifications to stdout.
4
4
  *
5
5
  * One JSON object per line. stderr is reserved for logger output.
6
+ * Line handling lives in JsonRpcConnection (shared with DaemonServer).
6
7
  */
7
8
 
8
- import readline from "readline";
9
9
  import type { Readable, Writable } from "stream";
10
10
  import { AgentBridge, type AgentBridgeOptions } from "./agentBridge.js";
11
- import {
12
- type JsonRpcRequest,
13
- type JsonRpcResponse,
14
- type JsonRpcNotification,
15
- PARSE_ERROR,
16
- INVALID_REQUEST,
17
- INTERNAL_ERROR,
18
- isRequest,
19
- isNotification,
20
- } from "./protocol.js";
11
+ import { JsonRpcConnection } from "./jsonRpcConnection.js";
21
12
 
22
13
  export interface StdioServerOptions {
23
14
  input?: Readable;
@@ -26,20 +17,20 @@ export interface StdioServerOptions {
26
17
  }
27
18
 
28
19
  export class StdioServer {
29
- private rl: readline.Interface | undefined;
30
20
  private bridge: AgentBridge;
31
- private input: Readable;
32
- private output: Writable;
33
- private started = false;
21
+ private conn: JsonRpcConnection;
34
22
 
35
23
  constructor(options: StdioServerOptions = {}) {
36
- this.input = options.input ?? process.stdin;
37
- this.output = options.output ?? process.stdout;
38
24
  this.bridge = new AgentBridge({
39
25
  ...options.bridgeOptions,
40
26
  emit: (method, params, sessionId) =>
41
27
  this.sendNotification(method, params, sessionId),
42
28
  });
29
+ this.conn = new JsonRpcConnection(
30
+ options.input ?? process.stdin,
31
+ options.output ?? process.stdout,
32
+ this.bridge,
33
+ );
43
34
  }
44
35
 
45
36
  get agentBridge(): AgentBridge {
@@ -47,123 +38,26 @@ export class StdioServer {
47
38
  }
48
39
 
49
40
  start(): void {
50
- if (this.started) return;
51
- this.started = true;
52
-
53
- this.rl = readline.createInterface({
54
- input: this.input,
55
- crlfDelay: Infinity,
56
- });
57
-
58
- this.rl.on("line", (line: string) => {
59
- this.handleLine(line).catch((err) => {
60
- // Should never reach here — handleLine catches internally
61
- this.sendResponse(null, undefined, {
62
- code: INTERNAL_ERROR,
63
- message: `Unhandled error: ${(err as Error).message}`,
64
- });
65
- });
66
- });
67
-
68
- this.rl.on("close", () => {
69
- this.started = false;
70
- });
41
+ this.conn.start();
71
42
  }
72
43
 
73
44
  stop(): void {
74
- this.rl?.close();
75
- this.rl = undefined;
76
- this.started = false;
45
+ this.conn.stop();
77
46
  }
78
47
 
79
- async handleLine(line: string): Promise<void> {
80
- const trimmed = line.trim();
81
- if (!trimmed) return;
82
-
83
- let msg: unknown;
84
- try {
85
- msg = JSON.parse(trimmed);
86
- } catch {
87
- this.sendResponse(null, undefined, {
88
- code: PARSE_ERROR,
89
- message: "Parse error: invalid JSON",
90
- });
91
- return;
92
- }
93
-
94
- if (isRequest(msg)) {
95
- await this.handleRequest(msg);
96
- } else if (isNotification(msg)) {
97
- this.handleNotification(msg);
98
- } else {
99
- // Echo back the id if the message has one, otherwise null
100
- const id =
101
- typeof msg === "object" &&
102
- msg !== null &&
103
- "id" in msg &&
104
- (typeof (msg as { id: unknown }).id === "number" ||
105
- typeof (msg as { id: unknown }).id === "string")
106
- ? (msg as { id: number | string }).id
107
- : null;
108
- this.sendResponse(id, undefined, {
109
- code: INVALID_REQUEST,
110
- message: "Invalid request: must have 'method' field",
111
- });
112
- }
48
+ handleLine(line: string): Promise<void> {
49
+ return this.conn.handleLine(line);
113
50
  }
114
51
 
115
- private async handleRequest(msg: JsonRpcRequest): Promise<void> {
116
- try {
117
- const result = await this.bridge.handleRequest(
118
- msg.method,
119
- msg.params,
120
- msg.sessionId,
121
- );
122
- this.sendResponse(msg.id, result);
123
- } catch (err) {
124
- const code =
125
- err && typeof err === "object" && "code" in err
126
- ? (err as { code: number }).code
127
- : INTERNAL_ERROR;
128
- const message = err instanceof Error ? err.message : String(err);
129
- this.sendResponse(msg.id, undefined, { code, message });
130
- }
131
- }
132
-
133
- private handleNotification(msg: JsonRpcNotification): void {
134
- try {
135
- this.bridge.handleNotification(msg.method, msg.params);
136
- } catch (err) {
137
- // Notifications don't get responses, but we log to stderr
138
- process.stderr.write(
139
- `Error handling notification ${msg.method}: ${(err as Error).message}\n`,
140
- );
141
- }
142
- }
143
-
144
- // ── Output helpers ────────────────────────────────────────────
145
-
146
52
  sendResponse(
147
53
  id: number | string | null,
148
54
  result?: unknown,
149
55
  error?: { code: number; message: string },
150
56
  ): void {
151
- const response: JsonRpcResponse = { id };
152
- if (error) {
153
- response.error = error;
154
- } else {
155
- response.result = result ?? null;
156
- }
157
- this.write(response);
57
+ this.conn.sendResponse(id, result, error);
158
58
  }
159
59
 
160
60
  sendNotification(method: string, params?: unknown, sessionId?: string): void {
161
- const notification: JsonRpcNotification = { method, params };
162
- if (sessionId) notification.sessionId = sessionId;
163
- this.write(notification);
164
- }
165
-
166
- private write(obj: JsonRpcResponse | JsonRpcNotification): void {
167
- this.output.write(JSON.stringify(obj) + "\n");
61
+ this.conn.sendNotification(method, params, sessionId);
168
62
  }
169
63
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Bracketed paste (DECSET 2004) detector.
3
+ *
4
+ * When bracketed paste is enabled, terminals wrap pasted text in
5
+ * `\x1b[200~` (start) and `\x1b[201~` (end) markers, which lets the input
6
+ * pipeline distinguish "text was pasted" from "user typed keys" — most
7
+ * importantly, a pasted trailing `\r` must NOT be treated as an Enter key.
8
+ *
9
+ * ink delivers each stdin chunk through useInput after stripping ONE leading
10
+ * ESC from the parsed sequence (see use-input.js), so markers may arrive in
11
+ * either the raw form (`\x1b[200~`) or the stripped form (`[200~`), and may
12
+ * be split across chunks. The detector normalizes both forms and buffers
13
+ * partial markers until they complete (deferral is safe: a deferred partial
14
+ * is flushed unchanged if the next chunk does not complete a marker).
15
+ */
16
+ export type PasteProcessResult =
17
+ | { kind: "input"; input: string }
18
+ | { kind: "paste"; text: string; leadingInput?: string }
19
+ | { kind: "consume" };
20
+
21
+ export interface BracketedPasteDetector {
22
+ /**
23
+ * Feed one input chunk (as delivered by ink's useInput callback).
24
+ * - `input`: regular keystrokes, pass through to normal handling.
25
+ * - `paste`: completed bracketed paste; insert `text` without submitting.
26
+ * `leadingInput` (rare) is content that preceded the start marker in the
27
+ * same chunk and should be handled as regular input first.
28
+ * - `consume`: content of an in-flight paste (or an empty paste); do
29
+ * nothing with this chunk.
30
+ */
31
+ process(chunk: string): PasteProcessResult;
32
+ reset(): void;
33
+ }
34
+
35
+ const ESC = "\u001b";
36
+ const START_STRIPPED = "[200~";
37
+ const START_RAW = `${ESC}[200~`;
38
+ const END_STRIPPED = "[201~";
39
+ const END_RAW = `${ESC}[201~`;
40
+
41
+ const START_FORMS = [START_RAW, START_STRIPPED];
42
+ const END_FORMS = [END_RAW, END_STRIPPED];
43
+
44
+ /**
45
+ * Suffixes that may be the beginning of a marker split across chunks.
46
+ * Longest-first so the longest matching suffix is deferred (e.g. `[200`
47
+ * rather than `[20` for `...x[200`). Deferral is flush-equivalent: if the
48
+ * next chunk does not complete a marker, the deferred suffix is delivered
49
+ * as regular input unchanged.
50
+ */
51
+ const PARTIAL_SUFFIXES = [
52
+ `${ESC}[201`,
53
+ `${ESC}[200`,
54
+ `${ESC}[20`,
55
+ `${ESC}[2`,
56
+ "[201",
57
+ "[200",
58
+ "[20",
59
+ "[2",
60
+ ].sort((a, b) => b.length - a.length);
61
+
62
+ export function createBracketedPasteDetector(): BracketedPasteDetector {
63
+ let inPaste = false;
64
+ let buffer = ""; // paste content collected since the start marker
65
+ let pending = ""; // deferred partial marker suffix from a previous chunk
66
+
67
+ const findFirst = (
68
+ text: string,
69
+ forms: string[],
70
+ ): { index: number; length: number } | null => {
71
+ let best: { index: number; length: number } | null = null;
72
+ for (const form of forms) {
73
+ const index = text.indexOf(form);
74
+ if (index !== -1 && (best === null || index < best.index)) {
75
+ best = { index, length: form.length };
76
+ }
77
+ }
78
+ return best;
79
+ };
80
+
81
+ const endsWithPartial = (text: string): string | null => {
82
+ for (const prefix of PARTIAL_SUFFIXES) {
83
+ if (text.endsWith(prefix)) {
84
+ return prefix;
85
+ }
86
+ }
87
+ return null;
88
+ };
89
+
90
+ const process = (chunk: string): PasteProcessResult => {
91
+ let remaining = pending + chunk;
92
+ pending = "";
93
+ let leadingInput = "";
94
+ let pasteText: string | null = null;
95
+
96
+ while (remaining.length > 0) {
97
+ if (!inPaste) {
98
+ const start = findFirst(remaining, START_FORMS);
99
+ if (start) {
100
+ leadingInput += remaining.slice(0, start.index);
101
+ remaining = remaining.slice(start.index + start.length);
102
+ inPaste = true;
103
+ continue;
104
+ }
105
+ // Orphan end marker (start was missed, e.g. consumed by another
106
+ // input handler): treat the preceding text as paste so a trailing
107
+ // `\r` cannot trigger the coalesced-Enter submit heuristic.
108
+ const end = findFirst(remaining, END_FORMS);
109
+ if (end) {
110
+ pasteText =
111
+ (pasteText ?? "") + leadingInput + remaining.slice(0, end.index);
112
+ leadingInput = "";
113
+ remaining = remaining.slice(end.index + end.length);
114
+ continue;
115
+ }
116
+ const partial = endsWithPartial(remaining);
117
+ if (partial) {
118
+ pending = remaining.slice(remaining.length - partial.length);
119
+ remaining = remaining.slice(0, remaining.length - partial.length);
120
+ }
121
+ if (remaining.length > 0) {
122
+ leadingInput += remaining;
123
+ remaining = "";
124
+ }
125
+ break;
126
+ }
127
+
128
+ const end = findFirst(remaining, END_FORMS);
129
+ if (end) {
130
+ pasteText = (pasteText ?? "") + buffer + remaining.slice(0, end.index);
131
+ buffer = "";
132
+ inPaste = false;
133
+ remaining = remaining.slice(end.index + end.length);
134
+ continue;
135
+ }
136
+ const partial = endsWithPartial(remaining);
137
+ if (partial) {
138
+ pending = remaining.slice(remaining.length - partial.length);
139
+ remaining = remaining.slice(0, remaining.length - partial.length);
140
+ }
141
+ buffer += remaining;
142
+ remaining = "";
143
+ break;
144
+ }
145
+
146
+ if (inPaste) {
147
+ // Paste still in flight: hold the content, deliver nothing yet.
148
+ return { kind: "consume" };
149
+ }
150
+ if (pasteText !== null) {
151
+ const result: { kind: "paste"; text: string; leadingInput?: string } = {
152
+ kind: "paste",
153
+ text: pasteText,
154
+ };
155
+ if (leadingInput !== "") {
156
+ result.leadingInput = leadingInput;
157
+ }
158
+ return result;
159
+ }
160
+ return { kind: "input", input: leadingInput };
161
+ };
162
+
163
+ const reset = (): void => {
164
+ inPaste = false;
165
+ buffer = "";
166
+ pending = "";
167
+ };
168
+
169
+ return { process, reset };
170
+ }