@codewithdan/zingit 0.7.0 → 0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewithdan/zingit",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI-powered UI annotation tool - point, annotate, and let AI fix it",
5
5
  "type": "module",
6
6
  "engines": {
@@ -25,6 +25,9 @@
25
25
  "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
26
26
  "dev:server": "cd server && npm run dev",
27
27
  "dev:client": "cd client && npm run dev",
28
+ "test": "cd client && npm run test",
29
+ "test:watch": "cd client && npm run test:watch",
30
+ "test:ui": "cd client && npm run test:ui",
28
31
  "release": "npm version minor && npm run build && npm publish --access public"
29
32
  },
30
33
  "keywords": [
@@ -1,11 +1,10 @@
1
- import type { WebSocket } from 'ws';
2
- import type { Agent, AgentSession, BatchData, ImageContent } from '../types.js';
1
+ import type { Agent, AgentSession, BatchData, ImageContent, WebSocketRef } from '../types.js';
3
2
  export declare abstract class BaseAgent implements Agent {
4
3
  abstract name: string;
5
4
  abstract model: string;
6
5
  abstract start(): Promise<void>;
7
6
  abstract stop(): Promise<void>;
8
- abstract createSession(ws: WebSocket, projectDir: string): Promise<AgentSession>;
7
+ abstract createSession(wsRef: WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
9
8
  /**
10
9
  * Format a prompt with image metadata for agents that don't support native multimodal.
11
10
  * This adds text descriptions of the images to the prompt.
@@ -1,4 +1,3 @@
1
- import type { WebSocket } from 'ws';
2
1
  import { BaseAgent } from './base.js';
3
2
  import type { AgentSession } from '../types.js';
4
3
  export declare class ClaudeCodeAgent extends BaseAgent {
@@ -14,5 +13,5 @@ export declare class ClaudeCodeAgent extends BaseAgent {
14
13
  * Create a generator that yields the initial user message with optional images
15
14
  */
16
15
  private createMessageGenerator;
17
- createSession(ws: WebSocket, projectDir: string): Promise<AgentSession>;
16
+ createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
18
17
  }
@@ -52,14 +52,18 @@ export class ClaudeCodeAgent extends BaseAgent {
52
52
  session_id: '' // SDK will assign the actual session ID
53
53
  };
54
54
  }
55
- async createSession(ws, projectDir) {
55
+ async createSession(wsRef, projectDir, resumeSessionId) {
56
56
  const send = (data) => {
57
- if (ws.readyState === ws.OPEN) {
57
+ const ws = wsRef.current;
58
+ if (ws && ws.readyState === ws.OPEN) {
58
59
  ws.send(JSON.stringify(data));
59
60
  }
60
61
  };
61
62
  // Track session ID for conversation continuity (stable V1 resume feature)
62
- let sessionId;
63
+ // Start with provided sessionId if resuming previous conversation
64
+ let sessionId = resumeSessionId;
65
+ // Track sent content to avoid duplicates (in case SDK sends same message multiple times)
66
+ const sentContent = new Set();
63
67
  return {
64
68
  send: async (msg) => {
65
69
  try {
@@ -73,21 +77,31 @@ export class ClaudeCodeAgent extends BaseAgent {
73
77
  permissionMode: 'acceptEdits', // Auto-approve file edits (no interactive terminal)
74
78
  // Resume previous session if we have a session ID (enables follow-up conversations)
75
79
  ...(sessionId && { resume: sessionId }),
76
- systemPrompt: `You are a UI debugging assistant. When given annotations about UI elements,
77
- you search for the corresponding code using the selectors and HTML context provided,
78
- then make the requested changes. Be thorough in finding the right files and making precise edits.
80
+ systemPrompt: `You are a direct, efficient UI modification assistant.
79
81
 
80
- When screenshots are provided, use them to:
81
- - Better understand the visual context and styling of the elements
82
- - Identify the exact appearance that needs to be changed
83
- - Verify you're targeting the correct element based on its visual representation
82
+ CRITICAL EFFICIENCY RULES:
83
+ 1. Use the provided selector and HTML context to quickly locate the target element
84
+ 2. Make the requested change immediately - don't explore or explain unless there's ambiguity
85
+ 3. For simple changes (text, styles, attributes), be concise - just do it and confirm
86
+ 4. Only search/explore if the selector doesn't match or you need to understand complex context
87
+ 5. Avoid explaining what annotations are or describing the codebase unnecessarily
84
88
 
85
- IMPORTANT: Format all responses using markdown:
86
- - Use **bold** for emphasis on important points
87
- - Use numbered lists for sequential steps (1. 2. 3.)
88
- - Use bullet points for non-sequential items
89
- - Use code blocks with \`\`\`language syntax for code examples
90
- - Use inline \`code\` for file paths, selectors, and technical terms`
89
+ WHEN TO BE BRIEF (90% of cases):
90
+ - Text changes: Find, change, confirm (1-2 sentences)
91
+ - Style changes: Find, modify CSS, confirm
92
+ - Simple DOM changes: Make the change, state what you did
93
+
94
+ WHEN TO BE THOROUGH (10% of cases):
95
+ - Ambiguous selectors (multiple matches)
96
+ - Complex architectural changes
97
+ - Need to understand component interactions
98
+
99
+ Response format:
100
+ - **Action taken:** Brief statement of what changed
101
+ - **File:** Path to modified file
102
+ - **Summary:** 1-2 sentence confirmation
103
+
104
+ Use screenshots to verify you're targeting the right element, but don't over-explain their purpose.`
91
105
  }
92
106
  });
93
107
  // Process streaming response
@@ -97,14 +111,23 @@ IMPORTANT: Format all responses using markdown:
97
111
  // Capture session ID from init message for follow-up conversations
98
112
  if ('subtype' in message && message.subtype === 'init') {
99
113
  sessionId = message.session_id;
114
+ console.log('[Claude Agent] Session initialized:', sessionId);
100
115
  }
101
116
  break;
102
117
  case 'assistant':
103
- // Handle assistant message - extract text from BetaMessage content
104
- if (message.message?.content) {
105
- for (const block of message.message.content) {
106
- if (block.type === 'text') {
107
- send({ type: 'delta', content: block.text });
118
+ // Extract text content from assistant messages (content is nested in message.content)
119
+ if ('message' in message && message.message && 'content' in message.message) {
120
+ const content = message.message.content;
121
+ if (Array.isArray(content)) {
122
+ for (const block of content) {
123
+ if (block.type === 'text' && block.text) {
124
+ // Deduplicate content (SDK may replay conversation history)
125
+ const contentHash = `${block.text.substring(0, 100)}_${block.text.length}`;
126
+ if (!sentContent.has(contentHash)) {
127
+ sentContent.add(contentHash);
128
+ send({ type: 'delta', content: block.text });
129
+ }
130
+ }
108
131
  }
109
132
  }
110
133
  }
@@ -114,20 +137,30 @@ IMPORTANT: Format all responses using markdown:
114
137
  if (message.event?.type === 'content_block_delta') {
115
138
  const delta = message.event.delta;
116
139
  if (delta && 'text' in delta) {
140
+ console.log('[Claude Agent] Content block delta, text length:', delta.text.length);
117
141
  send({ type: 'delta', content: delta.text });
118
142
  }
119
143
  }
144
+ else if (message.event?.type === 'content_block_stop') {
145
+ console.log('[Claude Agent] Content block stopped');
146
+ }
147
+ else if (message.event?.type === 'message_stop') {
148
+ console.log('[Claude Agent] Message stopped');
149
+ }
120
150
  break;
121
151
  case 'tool_progress':
122
152
  // Tool is being executed
153
+ console.log('[Claude Agent] Tool executing:', message.tool_name);
123
154
  send({ type: 'tool_start', tool: message.tool_name });
124
155
  break;
125
156
  case 'result':
126
157
  // Query completed
158
+ console.log('[Claude Agent] Query completed, sending idle');
127
159
  send({ type: 'idle' });
128
160
  break;
129
161
  }
130
162
  }
163
+ console.log('[Claude Agent] Response stream ended');
131
164
  }
132
165
  catch (err) {
133
166
  send({ type: 'error', message: err.message });
@@ -135,7 +168,8 @@ IMPORTANT: Format all responses using markdown:
135
168
  },
136
169
  destroy: async () => {
137
170
  // SDK handles session cleanup automatically
138
- }
171
+ },
172
+ getSessionId: () => sessionId || null
139
173
  };
140
174
  }
141
175
  }
@@ -1,4 +1,3 @@
1
- import type { WebSocket } from 'ws';
2
1
  import { BaseAgent } from './base.js';
3
2
  import type { AgentSession } from '../types.js';
4
3
  export declare class CodexAgent extends BaseAgent {
@@ -8,5 +7,5 @@ export declare class CodexAgent extends BaseAgent {
8
7
  constructor();
9
8
  start(): Promise<void>;
10
9
  stop(): Promise<void>;
11
- createSession(ws: WebSocket, projectDir: string): Promise<AgentSession>;
10
+ createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
12
11
  }
@@ -24,19 +24,24 @@ export class CodexAgent extends BaseAgent {
24
24
  // Codex SDK doesn't require explicit cleanup
25
25
  this.codex = null;
26
26
  }
27
- async createSession(ws, projectDir) {
27
+ async createSession(wsRef, projectDir, resumeSessionId) {
28
28
  if (!this.codex) {
29
29
  throw new Error('Codex client not initialized');
30
30
  }
31
31
  const send = (data) => {
32
- if (ws.readyState === ws.OPEN) {
32
+ const ws = wsRef.current;
33
+ if (ws && ws.readyState === ws.OPEN) {
33
34
  ws.send(JSON.stringify(data));
34
35
  }
35
36
  };
36
- // Start a Codex thread with the project directory
37
- const thread = this.codex.startThread({
37
+ // Thread options for both new and resumed threads
38
+ const threadOptions = {
38
39
  workingDirectory: projectDir,
39
- });
40
+ };
41
+ // Resume existing thread if we have a thread ID, otherwise start new thread
42
+ const thread = resumeSessionId
43
+ ? this.codex.resumeThread(resumeSessionId, threadOptions)
44
+ : this.codex.startThread(threadOptions);
40
45
  let abortController = null;
41
46
  // Track temp files for cleanup on session destroy (prevents race condition)
42
47
  const sessionTempFiles = [];
@@ -132,6 +137,7 @@ IMPORTANT: Format all responses using markdown:
132
137
  break;
133
138
  case 'turn.completed':
134
139
  // Turn finished
140
+ console.log('[Codex Agent] Turn completed, sending idle');
135
141
  send({ type: 'idle' });
136
142
  break;
137
143
  case 'turn.failed':
@@ -143,6 +149,7 @@ IMPORTANT: Format all responses using markdown:
143
149
  break;
144
150
  }
145
151
  }
152
+ console.log('[Codex Agent] Event stream ended');
146
153
  }
147
154
  catch (err) {
148
155
  send({ type: 'error', message: err.message });
@@ -171,7 +178,8 @@ IMPORTANT: Format all responses using markdown:
171
178
  }
172
179
  sessionTempFiles.length = 0; // Clear the array
173
180
  }
174
- }
181
+ },
182
+ getSessionId: () => thread.id
175
183
  };
176
184
  }
177
185
  }
@@ -1,4 +1,3 @@
1
- import type { WebSocket } from 'ws';
2
1
  import { BaseAgent } from './base.js';
3
2
  import type { AgentSession } from '../types.js';
4
3
  export declare class CopilotAgent extends BaseAgent {
@@ -8,5 +7,5 @@ export declare class CopilotAgent extends BaseAgent {
8
7
  constructor();
9
8
  start(): Promise<void>;
10
9
  stop(): Promise<void>;
11
- createSession(ws: WebSocket, projectDir: string): Promise<AgentSession>;
10
+ createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
12
11
  }
@@ -29,18 +29,18 @@ export class CopilotAgent extends BaseAgent {
29
29
  this.client = null;
30
30
  }
31
31
  }
32
- async createSession(ws, projectDir) {
32
+ async createSession(wsRef, projectDir, resumeSessionId) {
33
33
  if (!this.client) {
34
34
  throw new Error('Copilot client not initialized');
35
35
  }
36
36
  const send = (data) => {
37
- if (ws.readyState === ws.OPEN) {
37
+ const ws = wsRef.current;
38
+ if (ws && ws.readyState === ws.OPEN) {
38
39
  ws.send(JSON.stringify(data));
39
40
  }
40
41
  };
41
- // Create a Copilot session with streaming enabled
42
- // Note: Copilot SDK doesn't support cwd in session config, so we include it in the system message
43
- const session = await this.client.createSession({
42
+ // System message and permission config for both new and resumed sessions
43
+ const sessionConfig = {
44
44
  model: this.model,
45
45
  streaming: true,
46
46
  systemMessage: {
@@ -75,7 +75,11 @@ IMPORTANT: Format all responses using markdown:
75
75
  }
76
76
  return { kind: 'approved' };
77
77
  },
78
- });
78
+ };
79
+ // Resume existing session if we have a sessionId, otherwise create new session
80
+ const session = resumeSessionId
81
+ ? await this.client.resumeSession(resumeSessionId, sessionConfig)
82
+ : await this.client.createSession(sessionConfig);
79
83
  // Track temp files for cleanup on session destroy (prevents race condition)
80
84
  const sessionTempFiles = [];
81
85
  // Subscribe to streaming events and capture unsubscribe function
@@ -87,17 +91,22 @@ IMPORTANT: Format all responses using markdown:
87
91
  break;
88
92
  case 'assistant.message':
89
93
  // Final message (we already sent deltas, so just log)
94
+ console.log('[Copilot Agent] Assistant message complete');
90
95
  break;
91
96
  case 'tool.execution_start':
97
+ console.log('[Copilot Agent] Tool executing:', event.data.toolName);
92
98
  send({ type: 'tool_start', tool: event.data.toolName });
93
99
  break;
94
100
  case 'tool.execution_complete':
101
+ console.log('[Copilot Agent] Tool complete:', event.data.toolCallId);
95
102
  send({ type: 'tool_end', tool: event.data.toolCallId });
96
103
  break;
97
104
  case 'session.idle':
105
+ console.log('[Copilot Agent] Session idle, sending idle message');
98
106
  send({ type: 'idle' });
99
107
  break;
100
108
  case 'session.error':
109
+ console.error('[Copilot Agent] Session error:', event.data.message);
101
110
  send({ type: 'error', message: event.data.message });
102
111
  break;
103
112
  }
@@ -105,6 +114,7 @@ IMPORTANT: Format all responses using markdown:
105
114
  return {
106
115
  send: async (msg) => {
107
116
  try {
117
+ console.log('[Copilot Agent] send() called, processing request...');
108
118
  // If images are provided, save them as temp files and attach them
109
119
  // Copilot SDK supports file attachments for images
110
120
  const attachments = [];
@@ -134,12 +144,15 @@ IMPORTANT: Format all responses using markdown:
134
144
  });
135
145
  }
136
146
  }
147
+ console.log('[Copilot Agent] Calling session.sendAndWait...');
137
148
  await session.sendAndWait({
138
149
  prompt: msg.prompt,
139
150
  attachments: attachments.length > 0 ? attachments : undefined
140
151
  });
152
+ console.log('[Copilot Agent] session.sendAndWait completed');
141
153
  }
142
154
  catch (err) {
155
+ console.error('[Copilot Agent] Error in send():', err.message);
143
156
  send({ type: 'error', message: err.message });
144
157
  }
145
158
  // Note: Temp files cleaned up on session destroy to avoid race condition
@@ -162,7 +175,8 @@ IMPORTANT: Format all responses using markdown:
162
175
  }
163
176
  sessionTempFiles.length = 0; // Clear the array
164
177
  }
165
- }
178
+ },
179
+ getSessionId: () => session.sessionId
166
180
  };
167
181
  }
168
182
  }
@@ -8,6 +8,8 @@ export interface ConnectionState {
8
8
  agent: Agent | null;
9
9
  gitManager: GitManager | null;
10
10
  currentCheckpointId: string | null;
11
+ sessionId: string | null;
12
+ wsRef: import('../types.js').WebSocketRef | null;
11
13
  }
12
14
  export interface MessageHandlerDeps {
13
15
  projectDir: string;
@@ -30,7 +32,7 @@ export declare function handleBatch(ws: WebSocket, state: ConnectionState, msg:
30
32
  /**
31
33
  * Handle message (follow-up message to agent)
32
34
  */
33
- export declare function handleMessage(ws: WebSocket, state: ConnectionState, msg: WSIncomingMessage): Promise<void>;
35
+ export declare function handleMessage(ws: WebSocket, state: ConnectionState, msg: WSIncomingMessage, deps: MessageHandlerDeps): Promise<void>;
34
36
  /**
35
37
  * Handle reset message
36
38
  */
@@ -110,9 +110,11 @@ export async function handleBatch(ws, state, msg, deps) {
110
110
  }
111
111
  // Use client-specified projectDir, or fall back to server default
112
112
  const projectDir = batchData.projectDir || deps.projectDir;
113
+ console.log('[Batch] ===== Request started =====');
113
114
  // Create a checkpoint before AI modifications (if git manager available)
114
115
  if (state.gitManager) {
115
116
  try {
117
+ console.log('[Batch] Creating checkpoint...');
116
118
  const checkpoint = await state.gitManager.createCheckpoint({
117
119
  annotations: batchData.annotations,
118
120
  pageUrl: batchData.pageUrl,
@@ -120,6 +122,7 @@ export async function handleBatch(ws, state, msg, deps) {
120
122
  agentName: state.agentName,
121
123
  });
122
124
  state.currentCheckpointId = checkpoint.id;
125
+ console.log('[Batch] Checkpoint created, sending to client');
123
126
  sendMessage(ws, {
124
127
  type: 'checkpoint_created',
125
128
  checkpoint: {
@@ -140,13 +143,60 @@ export async function handleBatch(ws, state, msg, deps) {
140
143
  console.warn('Failed to create checkpoint:', err.message);
141
144
  }
142
145
  }
146
+ console.log('[Batch] Creating session if needed...');
143
147
  if (!state.session) {
144
- state.session = await state.agent.createSession(ws, projectDir);
148
+ // Create or update WebSocket reference for reconnection support
149
+ if (!state.wsRef) {
150
+ const { WebSocketRef } = await import('../types.js');
151
+ state.wsRef = new WebSocketRef(ws);
152
+ }
153
+ else {
154
+ // Update existing reference with new WebSocket (reconnection case)
155
+ state.wsRef.current = ws;
156
+ }
157
+ // Pass the preserved sessionId to resume conversation history
158
+ state.session = await state.agent.createSession(state.wsRef, projectDir, state.sessionId || undefined);
159
+ console.log('[Batch] New session created', state.sessionId ? '(resuming previous conversation)' : '(fresh start)');
160
+ // Store the sessionId if the agent provides it
161
+ if (state.session.getSessionId) {
162
+ const newSessionId = state.session.getSessionId();
163
+ if (newSessionId && newSessionId !== state.sessionId) {
164
+ state.sessionId = newSessionId;
165
+ console.log('[Batch] Session ID captured for future resumption');
166
+ }
167
+ }
168
+ }
169
+ else {
170
+ console.log('[Batch] Reusing existing session');
171
+ }
172
+ // Log user's annotations before formatting
173
+ console.log('[Batch] Annotation count:', batchData.annotations?.length || 0);
174
+ if (batchData.annotations && batchData.annotations.length > 0) {
175
+ batchData.annotations.forEach((ann, idx) => {
176
+ const notePreview = ann.notes?.substring(0, 200) || '(no notes)';
177
+ console.log(`[Batch] Annotation ${idx + 1}: ${notePreview}`);
178
+ });
145
179
  }
180
+ if (batchData.pageUrl) {
181
+ console.log('[Batch] Page URL:', batchData.pageUrl);
182
+ }
183
+ console.log('[Batch] Formatting prompt and extracting images...');
146
184
  const prompt = state.agent.formatPrompt(batchData, projectDir);
147
185
  const images = state.agent.extractImages(batchData);
186
+ console.log('[Batch] Image count:', images.length);
187
+ console.log('[Batch] Sending processing message to client');
148
188
  sendMessage(ws, { type: 'processing' });
189
+ console.log('[Batch] Starting agent session.send()...');
149
190
  await state.session.send({ prompt, images: images.length > 0 ? images : undefined });
191
+ console.log('[Batch] Agent session.send() completed');
192
+ // Update sessionId after send (it may be assigned during the first message)
193
+ if (state.session.getSessionId) {
194
+ const currentSessionId = state.session.getSessionId();
195
+ if (currentSessionId && currentSessionId !== state.sessionId) {
196
+ state.sessionId = currentSessionId;
197
+ console.log('[Batch] Session ID updated after message');
198
+ }
199
+ }
150
200
  // Finalize checkpoint after processing
151
201
  if (state.gitManager && state.currentCheckpointId) {
152
202
  try {
@@ -166,30 +216,75 @@ export async function handleBatch(ws, state, msg, deps) {
166
216
  }
167
217
  state.currentCheckpointId = null;
168
218
  }
219
+ console.log('[Batch] ===== Request completed =====');
169
220
  }
170
221
  /**
171
222
  * Handle message (follow-up message to agent)
172
223
  */
173
- export async function handleMessage(ws, state, msg) {
174
- if (state.session && msg.content) {
175
- try {
176
- console.log(`[ZingIt] Sending follow-up message: "${msg.content.substring(0, 50)}..."`);
177
- sendMessage(ws, { type: 'processing' });
178
- // Add timeout to detect if SDK hangs
179
- const timeoutMs = 120000; // 2 minutes
180
- const sendPromise = state.session.send({ prompt: msg.content });
181
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Agent response timeout')), timeoutMs));
182
- await Promise.race([sendPromise, timeoutPromise]);
183
- console.log('[ZingIt] Follow-up message sent to agent');
224
+ export async function handleMessage(ws, state, msg, deps) {
225
+ if (!msg.content) {
226
+ return;
227
+ }
228
+ console.log('[Message] ===== Request started =====');
229
+ // Create session if it doesn't exist (allows direct messaging without annotations)
230
+ if (!state.session) {
231
+ if (!state.agent) {
232
+ console.warn('[ZingIt] No agent selected for message');
233
+ sendMessage(ws, { type: 'error', message: 'No agent selected. Please select an agent first.' });
234
+ return;
184
235
  }
185
- catch (err) {
186
- console.error('[ZingIt] Error sending follow-up message:', err.message);
187
- sendMessage(ws, { type: 'error', message: `Failed to send message: ${err.message}` });
236
+ console.log('[ZingIt] Creating session for direct message', state.sessionId ? '(resuming conversation)' : '(fresh start)');
237
+ // Create or update WebSocket reference for reconnection support
238
+ if (!state.wsRef) {
239
+ const { WebSocketRef } = await import('../types.js');
240
+ state.wsRef = new WebSocketRef(ws);
241
+ }
242
+ else {
243
+ // Update existing reference with new WebSocket (reconnection case)
244
+ state.wsRef.current = ws;
245
+ }
246
+ // Pass the preserved sessionId to resume conversation history
247
+ state.session = await state.agent.createSession(state.wsRef, deps.projectDir, state.sessionId || undefined);
248
+ // Store the sessionId if the agent provides it
249
+ if (state.session.getSessionId) {
250
+ const newSessionId = state.session.getSessionId();
251
+ if (newSessionId && newSessionId !== state.sessionId) {
252
+ state.sessionId = newSessionId;
253
+ console.log('[Message] Session ID captured for future resumption');
254
+ }
188
255
  }
189
256
  }
190
- else if (!state.session) {
191
- console.warn('[ZingIt] No active session for follow-up message');
192
- sendMessage(ws, { type: 'error', message: 'No active session. Please create annotations first.' });
257
+ try {
258
+ const messagePreview = msg.content.length > 500 ? msg.content.substring(0, 500) + '...' : msg.content;
259
+ console.log('[Message] User message:', messagePreview);
260
+ if (msg.pageUrl) {
261
+ console.log('[Message] Page URL:', msg.pageUrl);
262
+ }
263
+ sendMessage(ws, { type: 'processing' });
264
+ // Prepend page URL context if provided
265
+ let prompt = msg.content;
266
+ if (msg.pageUrl) {
267
+ prompt = `[Context: User is currently on page: ${msg.pageUrl}]\n\n${msg.content}`;
268
+ }
269
+ // Add timeout to detect if SDK hangs
270
+ const timeoutMs = 120000; // 2 minutes
271
+ const sendPromise = state.session.send({ prompt });
272
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Agent response timeout')), timeoutMs));
273
+ await Promise.race([sendPromise, timeoutPromise]);
274
+ console.log('[Message] Agent processing completed');
275
+ // Update sessionId after send (it may be assigned during the first message)
276
+ if (state.session && state.session.getSessionId) {
277
+ const currentSessionId = state.session.getSessionId();
278
+ if (currentSessionId && currentSessionId !== state.sessionId) {
279
+ state.sessionId = currentSessionId;
280
+ console.log('[Message] Session ID updated after message');
281
+ }
282
+ }
283
+ console.log('[Message] ===== Request completed =====');
284
+ }
285
+ catch (err) {
286
+ console.error('[Message] Error sending message:', err.message);
287
+ sendMessage(ws, { type: 'error', message: `Failed to send message: ${err.message}` });
193
288
  }
194
289
  }
195
290
  /**
@@ -94,6 +94,8 @@ async function main() {
94
94
  agent: DEFAULT_AGENT ? initializedAgents.get(DEFAULT_AGENT) || null : null,
95
95
  gitManager,
96
96
  currentCheckpointId: null,
97
+ sessionId: null, // Preserved across reconnections for conversation continuity
98
+ wsRef: null, // Will be created when first session is established
97
99
  };
98
100
  // Track cleanup timer to prevent race conditions
99
101
  let sessionCleanupTimer = null;
@@ -107,6 +109,11 @@ async function main() {
107
109
  sessionCleanupTimer = null;
108
110
  console.log('Cancelled session cleanup - client reconnected');
109
111
  }
112
+ // Update WebSocket reference if session exists (reconnection case)
113
+ if (state.wsRef) {
114
+ state.wsRef.current = ws;
115
+ console.log('Updated WebSocket reference for existing session');
116
+ }
110
117
  // Heartbeat mechanism to detect dead connections
111
118
  let isAlive = true;
112
119
  ws.on('pong', () => {
@@ -149,7 +156,7 @@ async function main() {
149
156
  await handleBatch(ws, state, msg, deps);
150
157
  break;
151
158
  case 'message':
152
- await handleMessage(ws, state, msg);
159
+ await handleMessage(ws, state, msg, deps);
153
160
  break;
154
161
  case 'reset':
155
162
  await handleReset(ws, state);
@@ -180,29 +187,32 @@ async function main() {
180
187
  connections.delete(ws);
181
188
  // Clean up heartbeat interval
182
189
  clearInterval(heartbeatInterval);
183
- // Don't destroy session immediately - keep it alive for reconnection (page reload)
184
- // Clear any existing cleanup timer to prevent race conditions
185
- if (sessionCleanupTimer) {
186
- clearTimeout(sessionCleanupTimer);
187
- sessionCleanupTimer = null;
188
- }
189
- // Set new cleanup timer only if no connections remain
190
- if (connections.size === 0) {
190
+ // Don't immediately destroy the session - it may still be processing
191
+ // Instead, schedule cleanup after a delay to allow for reconnection
192
+ // If client reconnects, the wsRef will be updated with the new WebSocket
193
+ if (state.session) {
194
+ console.log('Scheduling session cleanup (allowing time for reconnection)');
195
+ // Clear any existing cleanup timer
196
+ if (sessionCleanupTimer) {
197
+ clearTimeout(sessionCleanupTimer);
198
+ }
199
+ // Destroy session after 5 seconds if client doesn't reconnect
191
200
  sessionCleanupTimer = setTimeout(async () => {
192
- if (state.session && connections.size === 0) {
193
- console.log('Cleaning up stale session after 5 minutes of inactivity');
201
+ if (state.session) {
194
202
  try {
195
203
  await state.session.destroy();
204
+ console.log('Session destroyed after reconnection timeout');
196
205
  }
197
206
  catch (err) {
198
207
  console.error('Error destroying session during cleanup:', err.message);
199
208
  }
200
209
  finally {
201
210
  state.session = null;
202
- sessionCleanupTimer = null;
211
+ state.wsRef = null;
203
212
  }
204
213
  }
205
- }, 300000); // 5 minutes
214
+ sessionCleanupTimer = null;
215
+ }, 5000);
206
216
  }
207
217
  });
208
218
  ws.on('error', (err) => {