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
@@ -34,8 +34,8 @@ export declare const INVALID_REQUEST = -32600;
34
34
  export declare const METHOD_NOT_FOUND = -32601;
35
35
  export declare const INVALID_PARAMS = -32602;
36
36
  export declare const INTERNAL_ERROR = -32603;
37
- export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
37
+ export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "listPendingPermissions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
38
38
  export type ClientNotificationMethod = "permissionResponse";
39
- export type ServerNotificationMethod = "messagesChange" | "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange";
39
+ export type ServerNotificationMethod = "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange" | "btwContent";
40
40
  export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
41
41
  export declare function isNotification(msg: unknown): msg is JsonRpcNotification;
@@ -3,6 +3,7 @@
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
  import type { Readable, Writable } from "stream";
8
9
  import { AgentBridge, type AgentBridgeOptions } from "./agentBridge.js";
@@ -12,22 +13,16 @@ export interface StdioServerOptions {
12
13
  bridgeOptions?: AgentBridgeOptions;
13
14
  }
14
15
  export declare class StdioServer {
15
- private rl;
16
16
  private bridge;
17
- private input;
18
- private output;
19
- private started;
17
+ private conn;
20
18
  constructor(options?: StdioServerOptions);
21
19
  get agentBridge(): AgentBridge;
22
20
  start(): void;
23
21
  stop(): void;
24
22
  handleLine(line: string): Promise<void>;
25
- private handleRequest;
26
- private handleNotification;
27
23
  sendResponse(id: number | string | null, result?: unknown, error?: {
28
24
  code: number;
29
25
  message: string;
30
26
  }): void;
31
27
  sendNotification(method: string, params?: unknown, sessionId?: string): void;
32
- private write;
33
28
  }
@@ -3,125 +3,34 @@
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
- import readline from "readline";
8
8
  import { AgentBridge } from "./agentBridge.js";
9
- import { PARSE_ERROR, INVALID_REQUEST, INTERNAL_ERROR, isRequest, isNotification, } from "./protocol.js";
9
+ import { JsonRpcConnection } from "./jsonRpcConnection.js";
10
10
  export class StdioServer {
11
11
  constructor(options = {}) {
12
- this.started = false;
13
- this.input = options.input ?? process.stdin;
14
- this.output = options.output ?? process.stdout;
15
12
  this.bridge = new AgentBridge({
16
13
  ...options.bridgeOptions,
17
14
  emit: (method, params, sessionId) => this.sendNotification(method, params, sessionId),
18
15
  });
16
+ this.conn = new JsonRpcConnection(options.input ?? process.stdin, options.output ?? process.stdout, this.bridge);
19
17
  }
20
18
  get agentBridge() {
21
19
  return this.bridge;
22
20
  }
23
21
  start() {
24
- if (this.started)
25
- return;
26
- this.started = true;
27
- this.rl = readline.createInterface({
28
- input: this.input,
29
- crlfDelay: Infinity,
30
- });
31
- this.rl.on("line", (line) => {
32
- this.handleLine(line).catch((err) => {
33
- // Should never reach here — handleLine catches internally
34
- this.sendResponse(null, undefined, {
35
- code: INTERNAL_ERROR,
36
- message: `Unhandled error: ${err.message}`,
37
- });
38
- });
39
- });
40
- this.rl.on("close", () => {
41
- this.started = false;
42
- });
22
+ this.conn.start();
43
23
  }
44
24
  stop() {
45
- this.rl?.close();
46
- this.rl = undefined;
47
- this.started = false;
25
+ this.conn.stop();
48
26
  }
49
- async handleLine(line) {
50
- const trimmed = line.trim();
51
- if (!trimmed)
52
- return;
53
- let msg;
54
- try {
55
- msg = JSON.parse(trimmed);
56
- }
57
- catch {
58
- this.sendResponse(null, undefined, {
59
- code: PARSE_ERROR,
60
- message: "Parse error: invalid JSON",
61
- });
62
- return;
63
- }
64
- if (isRequest(msg)) {
65
- await this.handleRequest(msg);
66
- }
67
- else if (isNotification(msg)) {
68
- this.handleNotification(msg);
69
- }
70
- else {
71
- // Echo back the id if the message has one, otherwise null
72
- const id = typeof msg === "object" &&
73
- msg !== null &&
74
- "id" in msg &&
75
- (typeof msg.id === "number" ||
76
- typeof msg.id === "string")
77
- ? msg.id
78
- : null;
79
- this.sendResponse(id, undefined, {
80
- code: INVALID_REQUEST,
81
- message: "Invalid request: must have 'method' field",
82
- });
83
- }
27
+ handleLine(line) {
28
+ return this.conn.handleLine(line);
84
29
  }
85
- async handleRequest(msg) {
86
- try {
87
- const result = await this.bridge.handleRequest(msg.method, msg.params, msg.sessionId);
88
- this.sendResponse(msg.id, result);
89
- }
90
- catch (err) {
91
- const code = err && typeof err === "object" && "code" in err
92
- ? err.code
93
- : INTERNAL_ERROR;
94
- const message = err instanceof Error ? err.message : String(err);
95
- this.sendResponse(msg.id, undefined, { code, message });
96
- }
97
- }
98
- handleNotification(msg) {
99
- try {
100
- this.bridge.handleNotification(msg.method, msg.params);
101
- }
102
- catch (err) {
103
- // Notifications don't get responses, but we log to stderr
104
- process.stderr.write(`Error handling notification ${msg.method}: ${err.message}\n`);
105
- }
106
- }
107
- // ── Output helpers ────────────────────────────────────────────
108
30
  sendResponse(id, result, error) {
109
- const response = { id };
110
- if (error) {
111
- response.error = error;
112
- }
113
- else {
114
- response.result = result ?? null;
115
- }
116
- this.write(response);
31
+ this.conn.sendResponse(id, result, error);
117
32
  }
118
33
  sendNotification(method, params, sessionId) {
119
- const notification = { method, params };
120
- if (sessionId)
121
- notification.sessionId = sessionId;
122
- this.write(notification);
123
- }
124
- write(obj) {
125
- this.output.write(JSON.stringify(obj) + "\n");
34
+ this.conn.sendNotification(method, params, sessionId);
126
35
  }
127
36
  }
@@ -0,0 +1,39 @@
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";
18
+ input: string;
19
+ } | {
20
+ kind: "paste";
21
+ text: string;
22
+ leadingInput?: string;
23
+ } | {
24
+ kind: "consume";
25
+ };
26
+ export interface BracketedPasteDetector {
27
+ /**
28
+ * Feed one input chunk (as delivered by ink's useInput callback).
29
+ * - `input`: regular keystrokes, pass through to normal handling.
30
+ * - `paste`: completed bracketed paste; insert `text` without submitting.
31
+ * `leadingInput` (rare) is content that preceded the start marker in the
32
+ * same chunk and should be handled as regular input first.
33
+ * - `consume`: content of an in-flight paste (or an empty paste); do
34
+ * nothing with this chunk.
35
+ */
36
+ process(chunk: string): PasteProcessResult;
37
+ reset(): void;
38
+ }
39
+ export declare function createBracketedPasteDetector(): BracketedPasteDetector;
@@ -0,0 +1,122 @@
1
+ const ESC = "\u001b";
2
+ const START_STRIPPED = "[200~";
3
+ const START_RAW = `${ESC}[200~`;
4
+ const END_STRIPPED = "[201~";
5
+ const END_RAW = `${ESC}[201~`;
6
+ const START_FORMS = [START_RAW, START_STRIPPED];
7
+ const END_FORMS = [END_RAW, END_STRIPPED];
8
+ /**
9
+ * Suffixes that may be the beginning of a marker split across chunks.
10
+ * Longest-first so the longest matching suffix is deferred (e.g. `[200`
11
+ * rather than `[20` for `...x[200`). Deferral is flush-equivalent: if the
12
+ * next chunk does not complete a marker, the deferred suffix is delivered
13
+ * as regular input unchanged.
14
+ */
15
+ const PARTIAL_SUFFIXES = [
16
+ `${ESC}[201`,
17
+ `${ESC}[200`,
18
+ `${ESC}[20`,
19
+ `${ESC}[2`,
20
+ "[201",
21
+ "[200",
22
+ "[20",
23
+ "[2",
24
+ ].sort((a, b) => b.length - a.length);
25
+ export function createBracketedPasteDetector() {
26
+ let inPaste = false;
27
+ let buffer = ""; // paste content collected since the start marker
28
+ let pending = ""; // deferred partial marker suffix from a previous chunk
29
+ const findFirst = (text, forms) => {
30
+ let best = null;
31
+ for (const form of forms) {
32
+ const index = text.indexOf(form);
33
+ if (index !== -1 && (best === null || index < best.index)) {
34
+ best = { index, length: form.length };
35
+ }
36
+ }
37
+ return best;
38
+ };
39
+ const endsWithPartial = (text) => {
40
+ for (const prefix of PARTIAL_SUFFIXES) {
41
+ if (text.endsWith(prefix)) {
42
+ return prefix;
43
+ }
44
+ }
45
+ return null;
46
+ };
47
+ const process = (chunk) => {
48
+ let remaining = pending + chunk;
49
+ pending = "";
50
+ let leadingInput = "";
51
+ let pasteText = null;
52
+ while (remaining.length > 0) {
53
+ if (!inPaste) {
54
+ const start = findFirst(remaining, START_FORMS);
55
+ if (start) {
56
+ leadingInput += remaining.slice(0, start.index);
57
+ remaining = remaining.slice(start.index + start.length);
58
+ inPaste = true;
59
+ continue;
60
+ }
61
+ // Orphan end marker (start was missed, e.g. consumed by another
62
+ // input handler): treat the preceding text as paste so a trailing
63
+ // `\r` cannot trigger the coalesced-Enter submit heuristic.
64
+ const end = findFirst(remaining, END_FORMS);
65
+ if (end) {
66
+ pasteText =
67
+ (pasteText ?? "") + leadingInput + remaining.slice(0, end.index);
68
+ leadingInput = "";
69
+ remaining = remaining.slice(end.index + end.length);
70
+ continue;
71
+ }
72
+ const partial = endsWithPartial(remaining);
73
+ if (partial) {
74
+ pending = remaining.slice(remaining.length - partial.length);
75
+ remaining = remaining.slice(0, remaining.length - partial.length);
76
+ }
77
+ if (remaining.length > 0) {
78
+ leadingInput += remaining;
79
+ remaining = "";
80
+ }
81
+ break;
82
+ }
83
+ const end = findFirst(remaining, END_FORMS);
84
+ if (end) {
85
+ pasteText = (pasteText ?? "") + buffer + remaining.slice(0, end.index);
86
+ buffer = "";
87
+ inPaste = false;
88
+ remaining = remaining.slice(end.index + end.length);
89
+ continue;
90
+ }
91
+ const partial = endsWithPartial(remaining);
92
+ if (partial) {
93
+ pending = remaining.slice(remaining.length - partial.length);
94
+ remaining = remaining.slice(0, remaining.length - partial.length);
95
+ }
96
+ buffer += remaining;
97
+ remaining = "";
98
+ break;
99
+ }
100
+ if (inPaste) {
101
+ // Paste still in flight: hold the content, deliver nothing yet.
102
+ return { kind: "consume" };
103
+ }
104
+ if (pasteText !== null) {
105
+ const result = {
106
+ kind: "paste",
107
+ text: pasteText,
108
+ };
109
+ if (leadingInput !== "") {
110
+ result.leadingInput = leadingInput;
111
+ }
112
+ return result;
113
+ }
114
+ return { kind: "input", input: leadingInput };
115
+ };
116
+ const reset = () => {
117
+ inPaste = false;
118
+ buffer = "";
119
+ pending = "";
120
+ };
121
+ return { process, reset };
122
+ }
@@ -0,0 +1,34 @@
1
+ import type { Token, Tokens } from "marked";
2
+ export type CellAlign = "left" | "center" | "right" | null;
3
+ /**
4
+ * Wrap text to fit within a given width, returning array of lines.
5
+ * ANSI-aware: preserves styling across line breaks.
6
+ *
7
+ * @param hard - If true, break words that exceed width (needed when columns
8
+ * are narrower than the longest word). Default false.
9
+ */
10
+ export declare function wrapText(text: string, width: number, options?: {
11
+ hard?: boolean;
12
+ }): string[];
13
+ /**
14
+ * Pad `content` to `targetWidth` according to alignment. `displayWidth` is the
15
+ * visible width of `content` (caller computes this, e.g. via stringWidth on
16
+ * ANSI-stripped text, so ANSI codes in `content` don't affect padding).
17
+ */
18
+ export declare function padAligned(content: string, displayWidth: number, targetWidth: number, align: CellAlign | undefined): string;
19
+ /**
20
+ * Render a markdown table to a box-drawing ANSI string for the terminal.
21
+ * Handles terminal width by:
22
+ * 1. Calculating minimum column widths based on longest word
23
+ * 2. Distributing available space proportionally
24
+ * 3. Wrapping text within cells (no truncation)
25
+ * 4. Falling back to a vertical key-value format when rows would be too
26
+ * tall or the table still overflows the terminal width
27
+ *
28
+ * @param token - marked table token
29
+ * @param columns - terminal width
30
+ * @param formatInline - formats inline tokens to an ANSI string (the caller
31
+ * binds its marked parser so this module stays
32
+ * parser-agnostic and directly testable)
33
+ */
34
+ export declare function renderMarkdownTable(token: Tokens.Table, columns: number, formatInline: (tokens: Token[]) => string): string;