@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/client/dist/zingit-client.js +111 -91
- package/package.json +4 -1
- package/server/dist/agents/base.d.ts +2 -3
- package/server/dist/agents/claude.d.ts +1 -2
- package/server/dist/agents/claude.js +56 -22
- package/server/dist/agents/codex.d.ts +1 -2
- package/server/dist/agents/codex.js +14 -6
- package/server/dist/agents/copilot.d.ts +1 -2
- package/server/dist/agents/copilot.js +21 -7
- package/server/dist/handlers/messageHandlers.d.ts +3 -1
- package/server/dist/handlers/messageHandlers.js +113 -18
- package/server/dist/index.js +23 -13
- package/server/dist/types.d.ts +10 -1
- package/server/dist/types.js +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codewithdan/zingit",
|
|
3
|
-
"version": "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 {
|
|
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(
|
|
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(
|
|
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(
|
|
55
|
+
async createSession(wsRef, projectDir, resumeSessionId) {
|
|
56
56
|
const send = (data) => {
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
//
|
|
104
|
-
if (message.message
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
32
|
+
const ws = wsRef.current;
|
|
33
|
+
if (ws && ws.readyState === ws.OPEN) {
|
|
33
34
|
ws.send(JSON.stringify(data));
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
|
-
//
|
|
37
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
|
|
37
|
+
const ws = wsRef.current;
|
|
38
|
+
if (ws && ws.readyState === ws.OPEN) {
|
|
38
39
|
ws.send(JSON.stringify(data));
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
|
-
//
|
|
42
|
-
|
|
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
|
-
|
|
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 (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
/**
|
package/server/dist/index.js
CHANGED
|
@@ -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
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
211
|
+
state.wsRef = null;
|
|
203
212
|
}
|
|
204
213
|
}
|
|
205
|
-
|
|
214
|
+
sessionCleanupTimer = null;
|
|
215
|
+
}, 5000);
|
|
206
216
|
}
|
|
207
217
|
});
|
|
208
218
|
ws.on('error', (err) => {
|