@pencil-agent/nano-pencil 1.7.0 → 1.8.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.
@@ -4,25 +4,38 @@
4
4
  * Adapts MCP tools to work with NanoPencil's tool system.
5
5
  */
6
6
  import { formatGuidanceMessage, getAPIKeyGuidance } from "./mcp-guidance.js";
7
+ function toSafeToolName(fullName) {
8
+ const normalized = `mcp_${fullName.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
9
+ if (normalized.length <= 60)
10
+ return normalized;
11
+ let hash = 0;
12
+ for (let i = 0; i < fullName.length; i++) {
13
+ hash = (hash * 31 + fullName.charCodeAt(i)) | 0;
14
+ }
15
+ const suffix = Math.abs(hash).toString(36);
16
+ return `${normalized.slice(0, 50)}_${suffix}`;
17
+ }
7
18
  /**
8
19
  * Create a NanoPencil ToolDefinition from an MCP tool definition
9
20
  */
10
21
  export function createMCPTool(mcpClient, mcpTool) {
11
- const toolName = mcpTool.name; // Full name like "filesystem/read"
12
- const [serverId] = toolName.split("/");
22
+ const rawToolName = mcpTool.name; // Full name like "filesystem/read"
23
+ const toolName = toSafeToolName(rawToolName);
24
+ const [serverId] = rawToolName.split("/");
13
25
  return {
14
26
  name: toolName,
15
- label: toolName,
16
- description: mcpTool.description,
27
+ label: rawToolName,
28
+ description: `${mcpTool.description} (MCP: ${rawToolName})`,
17
29
  // Use TypeBox Object schema with any properties since MCP tools have dynamic schemas
18
- parameters: {
19
- type: "object",
20
- properties: {},
21
- additionalProperties: true,
22
- },
30
+ parameters: mcpTool.inputSchema ??
31
+ {
32
+ type: "object",
33
+ properties: {},
34
+ additionalProperties: true,
35
+ },
23
36
  async execute(toolCallId, params, signal, onUpdate, ctx) {
24
37
  try {
25
- const result = await mcpClient.callTool(toolName, params);
38
+ const result = await mcpClient.callTool(rawToolName, params);
26
39
  if (result.error) {
27
40
  // Check if error is due to missing API key and provide guidance
28
41
  const guidance = getAPIKeyGuidance(serverId);
@@ -79,9 +92,7 @@ export function createMCPTool(mcpClient, mcpTool) {
79
92
  };
80
93
  }
81
94
  return {
82
- content: [
83
- { type: "text", text: `Failed to call MCP tool ${toolName}` },
84
- ],
95
+ content: [{ type: "text", text: `Failed to call MCP tool ${rawToolName}` }],
85
96
  details: {
86
97
  error: error instanceof Error ? error.message : String(error),
87
98
  },
@@ -2,7 +2,7 @@
2
2
  * MCP (Model Context Protocol) Client
3
3
  *
4
4
  * Provides client functionality to connect to MCP servers and call their tools.
5
- * Supports both stdio and SSE transport types.
5
+ * Supports stdio transport with JSON-RPC framing.
6
6
  */
7
7
  export interface MCPServerConfig {
8
8
  /** Unique identifier for this server */
@@ -28,7 +28,7 @@ export interface MCPTool {
28
28
  /** Tool description */
29
29
  description: string;
30
30
  /** JSON Schema for input */
31
- inputSchema: any;
31
+ inputSchema: Record<string, unknown>;
32
32
  /** Server ID */
33
33
  serverId: string;
34
34
  }
@@ -50,7 +50,7 @@ export interface MCPToolResult {
50
50
  */
51
51
  export declare class MCPClient {
52
52
  private servers;
53
- private serverProcesses;
53
+ private serverRuntimes;
54
54
  private serverTools;
55
55
  constructor();
56
56
  /**
@@ -73,6 +73,16 @@ export declare class MCPClient {
73
73
  * Remove a server
74
74
  */
75
75
  removeServer(id: string): void;
76
+ private getRuntime;
77
+ private attachStdoutParser;
78
+ private processStdoutBuffer;
79
+ private handleJsonRpcMessage;
80
+ private writeFramedMessage;
81
+ private sendNotification;
82
+ private sendRequest;
83
+ private initializeServer;
84
+ private normalizeToolRecord;
85
+ private loadToolsForServer;
76
86
  /**
77
87
  * Start an MCP server (for stdio transport)
78
88
  */
@@ -92,7 +102,7 @@ export declare class MCPClient {
92
102
  /**
93
103
  * Call an MCP tool
94
104
  */
95
- callTool(toolName: string, args: Record<string, any>): Promise<MCPToolResult>;
105
+ callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolResult>;
96
106
  /**
97
107
  * Call tool via stdio (JSON-RPC)
98
108
  */
@@ -2,7 +2,7 @@
2
2
  * MCP (Model Context Protocol) Client
3
3
  *
4
4
  * Provides client functionality to connect to MCP servers and call their tools.
5
- * Supports both stdio and SSE transport types.
5
+ * Supports stdio transport with JSON-RPC framing.
6
6
  */
7
7
  import { spawn } from "child_process";
8
8
  import { existsSync, readFileSync } from "fs";
@@ -14,7 +14,7 @@ import { getAgentDir } from "../../config.js";
14
14
  */
15
15
  export class MCPClient {
16
16
  servers = new Map();
17
- serverProcesses = new Map();
17
+ serverRuntimes = new Map();
18
18
  serverTools = new Map();
19
19
  constructor() {
20
20
  this.loadServersFromConfig();
@@ -67,6 +67,202 @@ export class MCPClient {
67
67
  this.serverTools.delete(id);
68
68
  this.stopServer(id);
69
69
  }
70
+ getRuntime(serverId) {
71
+ return this.serverRuntimes.get(serverId);
72
+ }
73
+ attachStdoutParser(serverId, runtime) {
74
+ runtime.process.stdout.on("data", (chunk) => {
75
+ runtime.buffer = Buffer.concat([runtime.buffer, chunk]);
76
+ this.processStdoutBuffer(serverId, runtime);
77
+ });
78
+ runtime.process.stderr.on("data", (chunk) => {
79
+ const text = chunk.toString("utf8").trim();
80
+ if (text.length > 0) {
81
+ console.error(`[MCP:${serverId}] ${text}`);
82
+ }
83
+ });
84
+ runtime.process.on("exit", (code, signal) => {
85
+ const message = `MCP server ${serverId} exited (code=${code ?? "null"}, signal=${signal ?? "null"})`;
86
+ for (const pending of runtime.pendingRequests.values()) {
87
+ clearTimeout(pending.timer);
88
+ pending.reject(new Error(message));
89
+ }
90
+ runtime.pendingRequests.clear();
91
+ this.serverRuntimes.delete(serverId);
92
+ });
93
+ }
94
+ processStdoutBuffer(serverId, runtime) {
95
+ // MCP stdio framing: "Content-Length: N\r\n\r\n<json>"
96
+ while (runtime.buffer.length > 0) {
97
+ const headerEnd = runtime.buffer.indexOf("\r\n\r\n");
98
+ if (headerEnd !== -1) {
99
+ const headerText = runtime.buffer.slice(0, headerEnd).toString("utf8");
100
+ const lengthMatch = headerText.match(/content-length:\s*(\d+)/i);
101
+ if (!lengthMatch) {
102
+ runtime.buffer = runtime.buffer.slice(headerEnd + 4);
103
+ continue;
104
+ }
105
+ const bodyLength = Number(lengthMatch[1]);
106
+ const totalLength = headerEnd + 4 + bodyLength;
107
+ if (runtime.buffer.length < totalLength) {
108
+ return;
109
+ }
110
+ const body = runtime.buffer
111
+ .slice(headerEnd + 4, totalLength)
112
+ .toString("utf8");
113
+ runtime.buffer = runtime.buffer.slice(totalLength);
114
+ this.handleJsonRpcMessage(serverId, body);
115
+ continue;
116
+ }
117
+ // Fallback for line-delimited JSON
118
+ const lineEnd = runtime.buffer.indexOf("\n");
119
+ if (lineEnd === -1) {
120
+ return;
121
+ }
122
+ const line = runtime.buffer.slice(0, lineEnd).toString("utf8").trim();
123
+ runtime.buffer = runtime.buffer.slice(lineEnd + 1);
124
+ if (line.length > 0) {
125
+ this.handleJsonRpcMessage(serverId, line);
126
+ }
127
+ }
128
+ }
129
+ handleJsonRpcMessage(serverId, raw) {
130
+ let msg;
131
+ try {
132
+ msg = JSON.parse(raw);
133
+ }
134
+ catch {
135
+ return;
136
+ }
137
+ if (msg.id === undefined) {
138
+ // Notification/unsolicited message
139
+ return;
140
+ }
141
+ const id = typeof msg.id === "number" ? msg.id : Number(msg.id);
142
+ if (!Number.isFinite(id)) {
143
+ return;
144
+ }
145
+ const runtime = this.serverRuntimes.get(serverId);
146
+ if (!runtime)
147
+ return;
148
+ const pending = runtime.pendingRequests.get(id);
149
+ if (!pending)
150
+ return;
151
+ clearTimeout(pending.timer);
152
+ runtime.pendingRequests.delete(id);
153
+ if (msg.error) {
154
+ pending.reject(new Error(msg.error.message || `JSON-RPC error ${msg.error.code ?? "unknown"}`));
155
+ return;
156
+ }
157
+ pending.resolve(msg.result);
158
+ }
159
+ writeFramedMessage(runtime, message) {
160
+ const body = JSON.stringify(message);
161
+ const framed = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
162
+ runtime.process.stdin.write(framed);
163
+ }
164
+ sendNotification(serverId, method, params) {
165
+ const runtime = this.getRuntime(serverId);
166
+ if (!runtime)
167
+ return;
168
+ this.writeFramedMessage(runtime, {
169
+ jsonrpc: "2.0",
170
+ method,
171
+ params: params ?? {},
172
+ });
173
+ }
174
+ async sendRequest(serverId, method, params, timeoutMs = 20_000) {
175
+ const runtime = this.getRuntime(serverId);
176
+ if (!runtime) {
177
+ throw new Error(`Server ${serverId} is not running`);
178
+ }
179
+ const id = runtime.nextRequestId++;
180
+ const payload = {
181
+ jsonrpc: "2.0",
182
+ id,
183
+ method,
184
+ params: params ?? {},
185
+ };
186
+ return new Promise((resolve, reject) => {
187
+ const timer = setTimeout(() => {
188
+ runtime.pendingRequests.delete(id);
189
+ reject(new Error(`MCP request timed out: ${serverId} ${method} (${timeoutMs}ms)`));
190
+ }, timeoutMs);
191
+ runtime.pendingRequests.set(id, {
192
+ resolve: (value) => resolve(value),
193
+ reject,
194
+ timer,
195
+ });
196
+ this.writeFramedMessage(runtime, payload);
197
+ });
198
+ }
199
+ async initializeServer(serverId) {
200
+ // Primary protocol version + fallback for older servers
201
+ const initPayload = {
202
+ protocolVersion: "2024-11-05",
203
+ capabilities: {},
204
+ clientInfo: { name: "nano-pencil", version: "1.7.0" },
205
+ };
206
+ const fallbackPayload = {
207
+ protocolVersion: "2024-10-07",
208
+ capabilities: {},
209
+ clientInfo: { name: "nano-pencil", version: "1.7.0" },
210
+ };
211
+ try {
212
+ await this.sendRequest(serverId, "initialize", initPayload, 20_000);
213
+ }
214
+ catch {
215
+ await this.sendRequest(serverId, "initialize", fallbackPayload, 20_000);
216
+ }
217
+ this.sendNotification(serverId, "notifications/initialized");
218
+ }
219
+ normalizeToolRecord(serverId, tool) {
220
+ if (!tool || typeof tool !== "object")
221
+ return null;
222
+ const obj = tool;
223
+ const name = obj.name;
224
+ const description = obj.description;
225
+ if (typeof name !== "string" || typeof description !== "string") {
226
+ return null;
227
+ }
228
+ const inputSchema = obj.inputSchema && typeof obj.inputSchema === "object"
229
+ ? obj.inputSchema
230
+ : { type: "object", properties: {}, additionalProperties: true };
231
+ return {
232
+ name: `${serverId}/${name}`,
233
+ displayName: typeof obj.title === "string"
234
+ ? obj.title
235
+ : typeof obj.displayName === "string"
236
+ ? obj.displayName
237
+ : undefined,
238
+ description,
239
+ inputSchema,
240
+ serverId,
241
+ };
242
+ }
243
+ async loadToolsForServer(serverId) {
244
+ const tools = [];
245
+ let cursor;
246
+ while (true) {
247
+ const result = (await this.sendRequest(serverId, "tools/list", cursor ? { cursor } : {}, 20_000));
248
+ const pageTools = Array.isArray(result.tools) ? result.tools : [];
249
+ for (const t of pageTools) {
250
+ const normalized = this.normalizeToolRecord(serverId, t);
251
+ if (normalized)
252
+ tools.push(normalized);
253
+ }
254
+ const nextCursor = typeof result.nextCursor === "string"
255
+ ? result.nextCursor
256
+ : typeof result.cursor === "string"
257
+ ? result.cursor
258
+ : undefined;
259
+ if (!nextCursor || nextCursor === cursor)
260
+ break;
261
+ cursor = nextCursor;
262
+ }
263
+ this.serverTools.set(serverId, tools);
264
+ return tools;
265
+ }
70
266
  /**
71
267
  * Start an MCP server (for stdio transport)
72
268
  */
@@ -80,7 +276,7 @@ export class MCPClient {
80
276
  return true;
81
277
  }
82
278
  // Check if already running
83
- if (this.serverProcesses.has(serverId)) {
279
+ if (this.serverRuntimes.has(serverId)) {
84
280
  return true;
85
281
  }
86
282
  try {
@@ -88,13 +284,21 @@ export class MCPClient {
88
284
  env: { ...process.env, ...server.env },
89
285
  stdio: ["pipe", "pipe", "pipe"],
90
286
  });
91
- this.serverProcesses.set(serverId, serverProcess);
92
- // TODO: Initialize MCP handshake, list tools
93
- // For now, we'll mark it as started
287
+ const runtime = {
288
+ process: serverProcess,
289
+ buffer: Buffer.alloc(0),
290
+ nextRequestId: 1,
291
+ pendingRequests: new Map(),
292
+ };
293
+ this.serverRuntimes.set(serverId, runtime);
294
+ this.attachStdoutParser(serverId, runtime);
295
+ await this.initializeServer(serverId);
296
+ await this.loadToolsForServer(serverId);
94
297
  return true;
95
298
  }
96
299
  catch (error) {
97
300
  console.error(`Failed to start MCP server ${serverId}:`, error);
301
+ this.stopServer(serverId);
98
302
  return false;
99
303
  }
100
304
  }
@@ -102,17 +306,22 @@ export class MCPClient {
102
306
  * Stop an MCP server
103
307
  */
104
308
  stopServer(serverId) {
105
- const process = this.serverProcesses.get(serverId);
106
- if (process) {
107
- process.kill();
108
- this.serverProcesses.delete(serverId);
309
+ const runtime = this.serverRuntimes.get(serverId);
310
+ if (runtime) {
311
+ for (const pending of runtime.pendingRequests.values()) {
312
+ clearTimeout(pending.timer);
313
+ pending.reject(new Error(`MCP server ${serverId} stopped`));
314
+ }
315
+ runtime.pendingRequests.clear();
316
+ runtime.process.kill();
317
+ this.serverRuntimes.delete(serverId);
109
318
  }
110
319
  }
111
320
  /**
112
321
  * Stop all running servers
113
322
  */
114
323
  stopAllServers() {
115
- for (const serverId of this.serverProcesses.keys()) {
324
+ for (const serverId of this.serverRuntimes.keys()) {
116
325
  this.stopServer(serverId);
117
326
  }
118
327
  }
@@ -122,12 +331,28 @@ export class MCPClient {
122
331
  async listTools(serverId) {
123
332
  const tools = [];
124
333
  if (serverId) {
334
+ if (!this.serverTools.has(serverId)) {
335
+ try {
336
+ await this.loadToolsForServer(serverId);
337
+ }
338
+ catch {
339
+ // keep cache miss as empty list
340
+ }
341
+ }
125
342
  const serverTools = this.serverTools.get(serverId) || [];
126
343
  tools.push(...serverTools);
127
344
  }
128
345
  else {
129
- for (const [_id, serverTools] of this.serverTools) {
130
- tools.push(...serverTools);
346
+ for (const [id] of this.servers.entries()) {
347
+ if (!this.serverTools.has(id)) {
348
+ try {
349
+ await this.loadToolsForServer(id);
350
+ }
351
+ catch {
352
+ // skip unavailable servers
353
+ }
354
+ }
355
+ tools.push(...(this.serverTools.get(id) || []));
131
356
  }
132
357
  }
133
358
  return tools;
@@ -146,7 +371,7 @@ export class MCPClient {
146
371
  error: `Server ${serverId} not found`,
147
372
  };
148
373
  }
149
- // For SSE transport, make HTTP request
374
+ // For SSE transport, make HTTP request (not implemented in current defaults)
150
375
  if (server.transport === "sse") {
151
376
  return this.callSSETool(server, toolNameOnly, args);
152
377
  }
@@ -157,34 +382,39 @@ export class MCPClient {
157
382
  * Call tool via stdio (JSON-RPC)
158
383
  */
159
384
  async callStdioTool(server, toolName, args) {
160
- const process = this.serverProcesses.get(server.id);
161
- if (!process) {
385
+ if (!this.serverRuntimes.has(server.id)) {
162
386
  return {
163
387
  content: [{ type: "text", text: `Server ${server.id} is not running` }],
164
388
  error: `Server ${server.id} is not running`,
165
389
  };
166
390
  }
167
391
  try {
168
- // Send JSON-RPC request
169
- const request = {
170
- jsonrpc: "2.0",
171
- id: Date.now(),
172
- method: "tools/call",
173
- params: {
174
- name: toolName,
175
- arguments: args,
176
- },
177
- };
178
- process.stdin.write(JSON.stringify(request) + "\n");
179
- // Read response (simplified - in production, use proper message handling)
180
- // For now, return a placeholder
392
+ const result = (await this.sendRequest(server.id, "tools/call", { name: toolName, arguments: args }, 60_000));
393
+ const isError = result.isError === true;
394
+ const content = Array.isArray(result.content)
395
+ ? result.content.map((item) => {
396
+ const type = item.type === "image" || item.type === "resource"
397
+ ? item.type
398
+ : "text";
399
+ return {
400
+ type: type,
401
+ text: typeof item.text === "string"
402
+ ? item.text
403
+ : typeof item.message === "string"
404
+ ? item.message
405
+ : undefined,
406
+ data: item,
407
+ };
408
+ })
409
+ : [{ type: "text", text: JSON.stringify(result) }];
181
410
  return {
182
- content: [
183
- {
184
- type: "text",
185
- text: `Tool ${toolName} called (TODO: implement response handling)`,
186
- },
187
- ],
411
+ content,
412
+ error: isError
413
+ ? content
414
+ .map((c) => c.text)
415
+ .filter((t) => !!t)
416
+ .join("\n") || `MCP tool ${toolName} failed`
417
+ : undefined,
188
418
  };
189
419
  }
190
420
  catch (error) {
@@ -200,7 +430,12 @@ export class MCPClient {
200
430
  async callSSETool(server, toolName, args) {
201
431
  // TODO: Implement SSE tool calls
202
432
  return {
203
- content: [{ type: "text", text: `SSE tool calls not yet implemented` }],
433
+ content: [
434
+ {
435
+ type: "text",
436
+ text: `SSE transport is not implemented yet for ${server.id}/${toolName}`,
437
+ },
438
+ ],
204
439
  error: "SSE transport not yet supported",
205
440
  };
206
441
  }
@@ -4,6 +4,7 @@
4
4
  * Manages MCP client lifecycle and tool integration.
5
5
  */
6
6
  import { MCPClient } from "./mcp/mcp-client.js";
7
+ import { loadMCPTools } from "./mcp/mcp-adapter.js";
7
8
  import { listEnabledMCPServers } from "./mcp/mcp-config.js";
8
9
  export class MCPManager {
9
10
  client;
@@ -25,8 +26,7 @@ export class MCPManager {
25
26
  }
26
27
  }
27
28
  // Load tools from all servers
28
- // TODO: MCP tool integration disabled due to type compatibility issues
29
- // this.tools = await loadMCPTools(this.client);
29
+ this.tools = await loadMCPTools(this.client);
30
30
  }
31
31
  /**
32
32
  * Get all MCP tools as NanoPencil ToolDefinitions
package/dist/core/sdk.js CHANGED
@@ -234,15 +234,14 @@ export async function createAgentSession(options = {}) {
234
234
  }
235
235
  // Initialize MCP if enabled (before creating AgentSession)
236
236
  let mcpManager;
237
- // TODO: MCP tool integration disabled due to type compatibility issues
238
- // Will be re-enabled after refactoring tool adapter
239
- // const mcpTools: ToolDefinition[] = [];
237
+ const mcpTools = [];
240
238
  if (options.enableMCP) {
241
239
  try {
242
240
  mcpManager = new MCPManager();
243
241
  await mcpManager.initialize();
244
- // mcpTools.push(...mcpManager.getTools());
242
+ mcpTools.push(...mcpManager.getTools());
245
243
  time("mcp.initialize");
244
+ process.once("exit", () => mcpManager?.dispose());
246
245
  }
247
246
  catch (error) {
248
247
  console.warn(`Failed to initialize MCP: ${error}`);
@@ -273,7 +272,7 @@ export async function createAgentSession(options = {}) {
273
272
  cwd,
274
273
  scopedModels: options.scopedModels,
275
274
  resourceLoader,
276
- customTools: options.customTools,
275
+ customTools: [...mcpTools, ...(options.customTools ?? [])],
277
276
  modelRegistry,
278
277
  initialActiveToolNames,
279
278
  extensionRunnerRef,
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@pencil-agent/nanomem",
3
+ "version": "1.0.0",
4
+ "private": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@pencil-agent/nanosoul",
3
+ "version": "1.0.0",
4
+ "private": true
5
+ }
package/dist/main.js CHANGED
@@ -617,6 +617,8 @@ export async function main(args) {
617
617
  sessionManager = SessionManager.open(selectedPath);
618
618
  }
619
619
  const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, settingsManager);
620
+ // NanoPencil 默认启用 MCP;离线模式下关闭以避免启动阻塞。
621
+ sessionOptions.enableMCP = APP_NAME === "nanopencil" && !offlineMode;
620
622
  sessionOptions.authStorage = authStorage;
621
623
  sessionOptions.modelRegistry = modelRegistry;
622
624
  sessionOptions.resourceLoader = resourceLoader;
@@ -61,6 +61,7 @@ export declare class InteractiveMode {
61
61
  private retryLoader;
62
62
  private retryEscapeHandler?;
63
63
  private compactionQueuedMessages;
64
+ private optimisticUserMessages;
64
65
  private shutdownRequested;
65
66
  private extensionSelector;
66
67
  private extensionInput;
@@ -112,6 +112,8 @@ export class InteractiveMode {
112
112
  retryEscapeHandler;
113
113
  // Messages queued while compaction is running
114
114
  compactionQueuedMessages = [];
115
+ // User messages rendered optimistically before Agent emits message_start
116
+ optimisticUserMessages = [];
115
117
  // Shutdown state
116
118
  shutdownRequested = false;
117
119
  // Extension UI state
@@ -1710,10 +1712,24 @@ export class InteractiveMode {
1710
1712
  }
1711
1713
  this.editor.addToHistory?.(text);
1712
1714
  this.editor.setText("");
1715
+ if (!text.startsWith("/")) {
1716
+ this.optimisticUserMessages.push(text);
1717
+ this.addMessageToChat({
1718
+ role: "user",
1719
+ content: [{ type: "text", text }],
1720
+ timestamp: Date.now(),
1721
+ });
1722
+ this.ui.requestRender();
1723
+ }
1713
1724
  try {
1714
1725
  await this.session.prompt(text);
1715
1726
  }
1716
1727
  catch (error) {
1728
+ if (!text.startsWith("/") &&
1729
+ this.optimisticUserMessages.length > 0 &&
1730
+ this.optimisticUserMessages[0] === text) {
1731
+ this.optimisticUserMessages.shift();
1732
+ }
1717
1733
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1718
1734
  this.showError(errorMessage);
1719
1735
  }
@@ -1764,6 +1780,15 @@ export class InteractiveMode {
1764
1780
  this.ui.requestRender();
1765
1781
  }
1766
1782
  else if (event.message.role === "user") {
1783
+ const userText = this.getUserMessageText(event.message);
1784
+ if (userText &&
1785
+ this.optimisticUserMessages.length > 0 &&
1786
+ this.optimisticUserMessages[0] === userText) {
1787
+ this.optimisticUserMessages.shift();
1788
+ this.updatePendingMessagesDisplay();
1789
+ this.ui.requestRender();
1790
+ break;
1791
+ }
1767
1792
  this.addMessageToChat(event.message);
1768
1793
  this.updatePendingMessagesDisplay();
1769
1794
  this.ui.requestRender();