@codewithdan/zingit 0.17.4 → 0.17.6
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/AGENTS.md +17 -14
- package/CHANGELOG.md +23 -1
- package/README.md +6 -14
- package/client/dist/zingit-client.js +226 -30
- package/package.json +21 -20
- package/server/dist/agents/core-adapter.d.ts +16 -0
- package/server/dist/agents/core-adapter.js +152 -0
- package/server/dist/handlers/messageHandlers.js +2 -1
- package/server/dist/index.js +106 -62
- package/server/dist/services/git-manager.d.ts +8 -0
- package/server/dist/types.d.ts +8 -0
- package/server/dist/agents/claude.d.ts +0 -17
- package/server/dist/agents/claude.js +0 -175
- package/server/dist/agents/codex.d.ts +0 -11
- package/server/dist/agents/codex.js +0 -202
- package/server/dist/agents/copilot.d.ts +0 -11
- package/server/dist/agents/copilot.js +0 -182
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
// server/src/agents/claude.ts
|
|
2
|
-
// Agent that uses Claude Agent SDK
|
|
3
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
-
import { BaseAgent } from './base.js';
|
|
5
|
-
export class ClaudeCodeAgent extends BaseAgent {
|
|
6
|
-
name = 'claude';
|
|
7
|
-
model = 'claude-sonnet-4-20250514';
|
|
8
|
-
async start() {
|
|
9
|
-
console.log(`✓ Claude Agent SDK initialized (model: ${this.model})`);
|
|
10
|
-
}
|
|
11
|
-
async stop() {
|
|
12
|
-
// SDK handles cleanup automatically
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Build content blocks for multimodal message with images and text
|
|
16
|
-
*/
|
|
17
|
-
buildContentBlocks(prompt, images) {
|
|
18
|
-
const content = [];
|
|
19
|
-
// Add images first so Claude sees them before the text instructions
|
|
20
|
-
if (images && images.length > 0) {
|
|
21
|
-
for (const img of images) {
|
|
22
|
-
// Add label as text before each image for context
|
|
23
|
-
if (img.label) {
|
|
24
|
-
content.push({ type: 'text', text: `[${img.label}]` });
|
|
25
|
-
}
|
|
26
|
-
content.push({
|
|
27
|
-
type: 'image',
|
|
28
|
-
source: {
|
|
29
|
-
type: 'base64',
|
|
30
|
-
media_type: img.mediaType,
|
|
31
|
-
data: img.base64
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// Add the main text prompt
|
|
37
|
-
content.push({ type: 'text', text: prompt });
|
|
38
|
-
return content;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Create a generator that yields the initial user message with optional images
|
|
42
|
-
*/
|
|
43
|
-
async *createMessageGenerator(prompt, images) {
|
|
44
|
-
const content = this.buildContentBlocks(prompt, images);
|
|
45
|
-
yield {
|
|
46
|
-
type: 'user',
|
|
47
|
-
message: {
|
|
48
|
-
role: 'user',
|
|
49
|
-
content
|
|
50
|
-
},
|
|
51
|
-
parent_tool_use_id: null,
|
|
52
|
-
session_id: '' // SDK will assign the actual session ID
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
async createSession(wsRef, projectDir, resumeSessionId) {
|
|
56
|
-
const send = (data) => {
|
|
57
|
-
const ws = wsRef.current;
|
|
58
|
-
if (ws && ws.readyState === ws.OPEN) {
|
|
59
|
-
ws.send(JSON.stringify(data));
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
// Track session ID for conversation continuity (stable V1 resume feature)
|
|
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();
|
|
67
|
-
return {
|
|
68
|
-
send: async (msg) => {
|
|
69
|
-
try {
|
|
70
|
-
// Use generator function to pass multimodal content (text + images)
|
|
71
|
-
const messageGenerator = this.createMessageGenerator(msg.prompt, msg.images);
|
|
72
|
-
const response = query({
|
|
73
|
-
prompt: messageGenerator,
|
|
74
|
-
options: {
|
|
75
|
-
model: this.model,
|
|
76
|
-
cwd: projectDir,
|
|
77
|
-
permissionMode: 'acceptEdits', // Auto-approve file edits (no interactive terminal)
|
|
78
|
-
// Resume previous session if we have a session ID (enables follow-up conversations)
|
|
79
|
-
...(sessionId && { resume: sessionId }),
|
|
80
|
-
systemPrompt: `You are a direct, efficient UI modification assistant.
|
|
81
|
-
|
|
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 markers are or describing the codebase unnecessarily
|
|
88
|
-
|
|
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.`
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
// Process streaming response
|
|
108
|
-
for await (const message of response) {
|
|
109
|
-
switch (message.type) {
|
|
110
|
-
case 'system':
|
|
111
|
-
// Capture session ID from init message for follow-up conversations
|
|
112
|
-
if ('subtype' in message && message.subtype === 'init') {
|
|
113
|
-
sessionId = message.session_id;
|
|
114
|
-
console.log('[Claude Agent] Session initialized:', sessionId);
|
|
115
|
-
}
|
|
116
|
-
break;
|
|
117
|
-
case 'assistant':
|
|
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
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
break;
|
|
135
|
-
case 'stream_event':
|
|
136
|
-
// Handle streaming events for real-time updates
|
|
137
|
-
if (message.event?.type === 'content_block_delta') {
|
|
138
|
-
const delta = message.event.delta;
|
|
139
|
-
if (delta && 'text' in delta) {
|
|
140
|
-
console.log('[Claude Agent] Content block delta, text length:', delta.text.length);
|
|
141
|
-
send({ type: 'delta', content: delta.text });
|
|
142
|
-
}
|
|
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
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
case 'tool_progress':
|
|
152
|
-
// Tool is being executed
|
|
153
|
-
console.log('[Claude Agent] Tool executing:', message.tool_name);
|
|
154
|
-
send({ type: 'tool_start', tool: message.tool_name });
|
|
155
|
-
break;
|
|
156
|
-
case 'result':
|
|
157
|
-
// Query completed
|
|
158
|
-
console.log('[Claude Agent] Query completed, sending idle');
|
|
159
|
-
send({ type: 'idle' });
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
console.log('[Claude Agent] Response stream ended');
|
|
164
|
-
}
|
|
165
|
-
catch (err) {
|
|
166
|
-
send({ type: 'error', message: err.message });
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
destroy: async () => {
|
|
170
|
-
// SDK handles session cleanup automatically
|
|
171
|
-
},
|
|
172
|
-
getSessionId: () => sessionId || null
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { BaseAgent } from './base.js';
|
|
2
|
-
import type { AgentSession } from '../types.js';
|
|
3
|
-
export declare class CodexAgent extends BaseAgent {
|
|
4
|
-
name: string;
|
|
5
|
-
model: string;
|
|
6
|
-
private codex;
|
|
7
|
-
constructor();
|
|
8
|
-
start(): Promise<void>;
|
|
9
|
-
stop(): Promise<void>;
|
|
10
|
-
createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
|
|
11
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
// server/src/agents/codex.ts
|
|
2
|
-
// Agent that uses OpenAI Codex SDK
|
|
3
|
-
import { Codex } from '@openai/codex-sdk';
|
|
4
|
-
import { BaseAgent } from './base.js';
|
|
5
|
-
import { promises as fs } from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
import * as os from 'os';
|
|
8
|
-
import { randomUUID } from 'crypto';
|
|
9
|
-
export class CodexAgent extends BaseAgent {
|
|
10
|
-
name = 'codex';
|
|
11
|
-
model;
|
|
12
|
-
codex = null;
|
|
13
|
-
constructor() {
|
|
14
|
-
super();
|
|
15
|
-
this.model = process.env.CODEX_MODEL || 'gpt-5.2-codex';
|
|
16
|
-
}
|
|
17
|
-
async start() {
|
|
18
|
-
// Initialize the Codex client
|
|
19
|
-
// Uses cached credentials from ~/.codex/auth.json (login via `codex` CLI)
|
|
20
|
-
this.codex = new Codex();
|
|
21
|
-
console.log(`✓ Codex SDK initialized (model: ${this.model})`);
|
|
22
|
-
}
|
|
23
|
-
async stop() {
|
|
24
|
-
// Codex SDK doesn't require explicit cleanup
|
|
25
|
-
this.codex = null;
|
|
26
|
-
}
|
|
27
|
-
async createSession(wsRef, projectDir, resumeSessionId) {
|
|
28
|
-
if (!this.codex) {
|
|
29
|
-
throw new Error('Codex client not initialized');
|
|
30
|
-
}
|
|
31
|
-
const send = (data) => {
|
|
32
|
-
const ws = wsRef.current;
|
|
33
|
-
if (ws && ws.readyState === ws.OPEN) {
|
|
34
|
-
ws.send(JSON.stringify(data));
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
// Thread options for both new and resumed threads
|
|
38
|
-
const threadOptions = {
|
|
39
|
-
workingDirectory: projectDir,
|
|
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);
|
|
45
|
-
let abortController = null;
|
|
46
|
-
// Track temp files for cleanup on session destroy (prevents race condition)
|
|
47
|
-
const sessionTempFiles = [];
|
|
48
|
-
return {
|
|
49
|
-
send: async (msg) => {
|
|
50
|
-
try {
|
|
51
|
-
abortController = new AbortController();
|
|
52
|
-
const input = [];
|
|
53
|
-
// If images are provided, save them as temp files and add to structured input
|
|
54
|
-
if (msg.images && msg.images.length > 0) {
|
|
55
|
-
const tempDir = os.tmpdir();
|
|
56
|
-
for (let i = 0; i < msg.images.length; i++) {
|
|
57
|
-
const img = msg.images[i];
|
|
58
|
-
// Use UUID to avoid filename collisions
|
|
59
|
-
const ext = img.mediaType.split('/')[1] || 'png';
|
|
60
|
-
const tempPath = path.join(tempDir, `zingit-screenshot-${randomUUID()}.${ext}`);
|
|
61
|
-
// Decode base64 to buffer with error handling
|
|
62
|
-
let buffer;
|
|
63
|
-
try {
|
|
64
|
-
buffer = Buffer.from(img.base64, 'base64');
|
|
65
|
-
}
|
|
66
|
-
catch (decodeErr) {
|
|
67
|
-
console.warn(`ZingIt: Failed to decode base64 for image ${i + 1}:`, decodeErr);
|
|
68
|
-
continue; // Skip this image
|
|
69
|
-
}
|
|
70
|
-
// Save with restrictive permissions (owner read/write only)
|
|
71
|
-
await fs.writeFile(tempPath, buffer, { mode: 0o600 });
|
|
72
|
-
sessionTempFiles.push(tempPath);
|
|
73
|
-
// Add label text before image
|
|
74
|
-
if (img.label) {
|
|
75
|
-
input.push({ type: 'text', text: `[${img.label}]` });
|
|
76
|
-
}
|
|
77
|
-
// Add image as local_image input
|
|
78
|
-
input.push({ type: 'local_image', path: tempPath });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// Add system instructions and main prompt
|
|
82
|
-
const systemInstructions = `You are a UI debugging assistant. When given markers about UI elements, search for the corresponding code using the selectors and HTML context provided, then make the requested changes.
|
|
83
|
-
|
|
84
|
-
When screenshots are provided, use them to:
|
|
85
|
-
- Better understand the visual context and styling of the elements
|
|
86
|
-
- Identify the exact appearance that needs to be changed
|
|
87
|
-
- Verify you're targeting the correct element based on its visual representation
|
|
88
|
-
|
|
89
|
-
IMPORTANT: Format all responses using markdown:
|
|
90
|
-
- Use **bold** for emphasis on important points
|
|
91
|
-
- Use numbered lists for sequential steps (1. 2. 3.)
|
|
92
|
-
- Use bullet points for non-sequential items
|
|
93
|
-
- Use code blocks with \`\`\`language syntax for code examples
|
|
94
|
-
- Use inline \`code\` for file paths, selectors, and technical terms
|
|
95
|
-
|
|
96
|
-
`;
|
|
97
|
-
input.push({ type: 'text', text: systemInstructions + msg.prompt });
|
|
98
|
-
// Use runStreamed with structured input for real-time progress
|
|
99
|
-
const { events } = await thread.runStreamed(input);
|
|
100
|
-
for await (const event of events) {
|
|
101
|
-
// Check if aborted
|
|
102
|
-
if (abortController?.signal.aborted) {
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
switch (event.type) {
|
|
106
|
-
case 'item.started':
|
|
107
|
-
// Tool/action started
|
|
108
|
-
if (event.item?.type) {
|
|
109
|
-
const toolName = getToolDisplayName(event.item);
|
|
110
|
-
send({ type: 'tool_start', tool: toolName });
|
|
111
|
-
}
|
|
112
|
-
break;
|
|
113
|
-
case 'item.completed':
|
|
114
|
-
// Item completed - extract content based on type
|
|
115
|
-
if (event.item) {
|
|
116
|
-
switch (event.item.type) {
|
|
117
|
-
case 'agent_message':
|
|
118
|
-
// Agent's text response
|
|
119
|
-
send({ type: 'delta', content: event.item.text + '\n' });
|
|
120
|
-
break;
|
|
121
|
-
case 'reasoning':
|
|
122
|
-
// Optional: show reasoning
|
|
123
|
-
send({ type: 'delta', content: `\n*[Reasoning]* ${event.item.text}\n` });
|
|
124
|
-
break;
|
|
125
|
-
case 'command_execution':
|
|
126
|
-
// Command was executed
|
|
127
|
-
send({ type: 'delta', content: `\n$ ${event.item.command}\n${event.item.aggregated_output}\n` });
|
|
128
|
-
break;
|
|
129
|
-
case 'file_change':
|
|
130
|
-
// Files were changed
|
|
131
|
-
const files = event.item.changes.map(c => `${c.kind}: ${c.path}`).join(', ');
|
|
132
|
-
send({ type: 'delta', content: `\n*[Files changed]* ${files}\n` });
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
send({ type: 'tool_end', tool: event.item.type });
|
|
136
|
-
}
|
|
137
|
-
break;
|
|
138
|
-
case 'turn.completed':
|
|
139
|
-
// Turn finished
|
|
140
|
-
console.log('[Codex Agent] Turn completed, sending idle');
|
|
141
|
-
send({ type: 'idle' });
|
|
142
|
-
break;
|
|
143
|
-
case 'turn.failed':
|
|
144
|
-
// Turn failed with error
|
|
145
|
-
send({ type: 'error', message: event.error?.message || 'Codex turn failed' });
|
|
146
|
-
break;
|
|
147
|
-
case 'error':
|
|
148
|
-
send({ type: 'error', message: event.message || 'Unknown Codex error' });
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
console.log('[Codex Agent] Event stream ended');
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
send({ type: 'error', message: err.message });
|
|
156
|
-
}
|
|
157
|
-
// Note: Temp files cleaned up on session destroy to avoid race condition
|
|
158
|
-
},
|
|
159
|
-
destroy: async () => {
|
|
160
|
-
try {
|
|
161
|
-
// Abort any ongoing operation
|
|
162
|
-
if (abortController) {
|
|
163
|
-
abortController.abort();
|
|
164
|
-
abortController = null;
|
|
165
|
-
}
|
|
166
|
-
// Thread cleanup happens automatically
|
|
167
|
-
}
|
|
168
|
-
finally {
|
|
169
|
-
// Clean up all temp files even if abort fails
|
|
170
|
-
for (const tempPath of sessionTempFiles) {
|
|
171
|
-
try {
|
|
172
|
-
await fs.unlink(tempPath);
|
|
173
|
-
}
|
|
174
|
-
catch (cleanupErr) {
|
|
175
|
-
// Ignore errors (file may already be deleted)
|
|
176
|
-
console.warn(`ZingIt: Failed to clean up temp file ${tempPath}:`, cleanupErr.message);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
sessionTempFiles.length = 0; // Clear the array
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
getSessionId: () => thread.id
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Helper to get a readable tool name
|
|
187
|
-
function getToolDisplayName(item) {
|
|
188
|
-
switch (item.type) {
|
|
189
|
-
case 'command_execution':
|
|
190
|
-
return `Running: ${item.command?.split(' ')[0] || 'command'}`;
|
|
191
|
-
case 'mcp_tool_call':
|
|
192
|
-
return `Tool: ${item.tool || 'mcp'}`;
|
|
193
|
-
case 'file_change':
|
|
194
|
-
return 'Editing files';
|
|
195
|
-
case 'web_search':
|
|
196
|
-
return 'Searching web';
|
|
197
|
-
case 'reasoning':
|
|
198
|
-
return 'Thinking...';
|
|
199
|
-
default:
|
|
200
|
-
return item.type;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { BaseAgent } from './base.js';
|
|
2
|
-
import type { AgentSession } from '../types.js';
|
|
3
|
-
export declare class CopilotAgent extends BaseAgent {
|
|
4
|
-
name: string;
|
|
5
|
-
model: string;
|
|
6
|
-
private client;
|
|
7
|
-
constructor();
|
|
8
|
-
start(): Promise<void>;
|
|
9
|
-
stop(): Promise<void>;
|
|
10
|
-
createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
|
|
11
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// server/src/agents/copilot.ts
|
|
2
|
-
// Agent that uses GitHub Copilot SDK
|
|
3
|
-
import { CopilotClient } from '@github/copilot-sdk';
|
|
4
|
-
import { BaseAgent } from './base.js';
|
|
5
|
-
import { promises as fs } from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
import * as os from 'os';
|
|
8
|
-
import { randomUUID } from 'crypto';
|
|
9
|
-
export class CopilotAgent extends BaseAgent {
|
|
10
|
-
name = 'copilot';
|
|
11
|
-
model;
|
|
12
|
-
client = null;
|
|
13
|
-
constructor() {
|
|
14
|
-
super();
|
|
15
|
-
this.model = process.env.COPILOT_MODEL || 'claude-sonnet-4-20250514';
|
|
16
|
-
}
|
|
17
|
-
async start() {
|
|
18
|
-
// Initialize the Copilot client
|
|
19
|
-
this.client = new CopilotClient({
|
|
20
|
-
logLevel: 'info',
|
|
21
|
-
autoRestart: true,
|
|
22
|
-
});
|
|
23
|
-
await this.client.start();
|
|
24
|
-
console.log(`✓ Copilot SDK initialized (model: ${this.model})`);
|
|
25
|
-
}
|
|
26
|
-
async stop() {
|
|
27
|
-
if (this.client) {
|
|
28
|
-
await this.client.stop();
|
|
29
|
-
this.client = null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async createSession(wsRef, projectDir, resumeSessionId) {
|
|
33
|
-
if (!this.client) {
|
|
34
|
-
throw new Error('Copilot client not initialized');
|
|
35
|
-
}
|
|
36
|
-
const send = (data) => {
|
|
37
|
-
const ws = wsRef.current;
|
|
38
|
-
if (ws && ws.readyState === ws.OPEN) {
|
|
39
|
-
ws.send(JSON.stringify(data));
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
// System message and permission config for both new and resumed sessions
|
|
43
|
-
const sessionConfig = {
|
|
44
|
-
model: this.model,
|
|
45
|
-
streaming: true,
|
|
46
|
-
systemMessage: {
|
|
47
|
-
mode: 'append',
|
|
48
|
-
content: `
|
|
49
|
-
<context>
|
|
50
|
-
You are a UI debugging assistant working in the project directory: ${projectDir}
|
|
51
|
-
|
|
52
|
-
When given markers about UI elements:
|
|
53
|
-
1. Search for the corresponding code using the selectors and HTML context provided
|
|
54
|
-
2. Make the requested changes in the project at ${projectDir}
|
|
55
|
-
3. Be thorough in finding the right files and making precise edits
|
|
56
|
-
|
|
57
|
-
When screenshots are provided, use them to:
|
|
58
|
-
- Better understand the visual context and styling of the elements
|
|
59
|
-
- Identify the exact appearance that needs to be changed
|
|
60
|
-
- Verify you're targeting the correct element based on its visual representation
|
|
61
|
-
|
|
62
|
-
IMPORTANT: Format all responses using markdown:
|
|
63
|
-
- Use **bold** for emphasis on important points
|
|
64
|
-
- Use numbered lists for sequential steps (1. 2. 3.)
|
|
65
|
-
- Use bullet points for non-sequential items
|
|
66
|
-
- Use code blocks with \`\`\`language syntax for code examples
|
|
67
|
-
- Use inline \`code\` for file paths, selectors, and technical terms
|
|
68
|
-
</context>
|
|
69
|
-
`
|
|
70
|
-
},
|
|
71
|
-
onPermissionRequest: async (request) => {
|
|
72
|
-
// Auto-approve read/write operations for file edits
|
|
73
|
-
if (request.kind === 'read' || request.kind === 'write') {
|
|
74
|
-
return { kind: 'approved' };
|
|
75
|
-
}
|
|
76
|
-
return { kind: 'approved' };
|
|
77
|
-
},
|
|
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);
|
|
83
|
-
// Track temp files for cleanup on session destroy (prevents race condition)
|
|
84
|
-
const sessionTempFiles = [];
|
|
85
|
-
// Subscribe to streaming events and capture unsubscribe function
|
|
86
|
-
const unsubscribe = session.on((event) => {
|
|
87
|
-
switch (event.type) {
|
|
88
|
-
case 'assistant.message_delta':
|
|
89
|
-
// Streaming chunk
|
|
90
|
-
send({ type: 'delta', content: event.data.deltaContent });
|
|
91
|
-
break;
|
|
92
|
-
case 'assistant.message':
|
|
93
|
-
// Final message (we already sent deltas, so just log)
|
|
94
|
-
console.log('[Copilot Agent] Assistant message complete');
|
|
95
|
-
break;
|
|
96
|
-
case 'tool.execution_start':
|
|
97
|
-
console.log('[Copilot Agent] Tool executing:', event.data.toolName);
|
|
98
|
-
send({ type: 'tool_start', tool: event.data.toolName });
|
|
99
|
-
break;
|
|
100
|
-
case 'tool.execution_complete':
|
|
101
|
-
console.log('[Copilot Agent] Tool complete:', event.data.toolCallId);
|
|
102
|
-
send({ type: 'tool_end', tool: event.data.toolCallId });
|
|
103
|
-
break;
|
|
104
|
-
case 'session.idle':
|
|
105
|
-
console.log('[Copilot Agent] Session idle, sending idle message');
|
|
106
|
-
send({ type: 'idle' });
|
|
107
|
-
break;
|
|
108
|
-
case 'session.error':
|
|
109
|
-
console.error('[Copilot Agent] Session error:', event.data.message);
|
|
110
|
-
send({ type: 'error', message: event.data.message });
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
return {
|
|
115
|
-
send: async (msg) => {
|
|
116
|
-
try {
|
|
117
|
-
console.log('[Copilot Agent] send() called, processing request...');
|
|
118
|
-
// If images are provided, save them as temp files and attach them
|
|
119
|
-
// Copilot SDK supports file attachments for images
|
|
120
|
-
const attachments = [];
|
|
121
|
-
if (msg.images && msg.images.length > 0) {
|
|
122
|
-
const tempDir = os.tmpdir();
|
|
123
|
-
for (let i = 0; i < msg.images.length; i++) {
|
|
124
|
-
const img = msg.images[i];
|
|
125
|
-
// Use UUID to avoid filename collisions
|
|
126
|
-
const ext = img.mediaType.split('/')[1] || 'png';
|
|
127
|
-
const tempPath = path.join(tempDir, `zingit-screenshot-${randomUUID()}.${ext}`);
|
|
128
|
-
// Decode base64 to buffer with error handling
|
|
129
|
-
let buffer;
|
|
130
|
-
try {
|
|
131
|
-
buffer = Buffer.from(img.base64, 'base64');
|
|
132
|
-
}
|
|
133
|
-
catch (decodeErr) {
|
|
134
|
-
console.warn(`ZingIt: Failed to decode base64 for image ${i + 1}:`, decodeErr);
|
|
135
|
-
continue; // Skip this image
|
|
136
|
-
}
|
|
137
|
-
// Save with restrictive permissions (owner read/write only)
|
|
138
|
-
await fs.writeFile(tempPath, buffer, { mode: 0o600 });
|
|
139
|
-
sessionTempFiles.push(tempPath);
|
|
140
|
-
attachments.push({
|
|
141
|
-
type: 'file',
|
|
142
|
-
path: tempPath,
|
|
143
|
-
displayName: img.label || `Screenshot ${i + 1}`
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
console.log('[Copilot Agent] Calling session.sendAndWait...');
|
|
148
|
-
await session.sendAndWait({
|
|
149
|
-
prompt: msg.prompt,
|
|
150
|
-
attachments: attachments.length > 0 ? attachments : undefined
|
|
151
|
-
});
|
|
152
|
-
console.log('[Copilot Agent] session.sendAndWait completed');
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
console.error('[Copilot Agent] Error in send():', err.message);
|
|
156
|
-
send({ type: 'error', message: err.message });
|
|
157
|
-
}
|
|
158
|
-
// Note: Temp files cleaned up on session destroy to avoid race condition
|
|
159
|
-
},
|
|
160
|
-
destroy: async () => {
|
|
161
|
-
try {
|
|
162
|
-
unsubscribe();
|
|
163
|
-
await session.destroy();
|
|
164
|
-
}
|
|
165
|
-
finally {
|
|
166
|
-
// Clean up all temp files even if destroy() fails
|
|
167
|
-
for (const tempPath of sessionTempFiles) {
|
|
168
|
-
try {
|
|
169
|
-
await fs.unlink(tempPath);
|
|
170
|
-
}
|
|
171
|
-
catch (cleanupErr) {
|
|
172
|
-
// Ignore errors (file may already be deleted)
|
|
173
|
-
console.warn(`ZingIt: Failed to clean up temp file ${tempPath}:`, cleanupErr.message);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
sessionTempFiles.length = 0; // Clear the array
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
getSessionId: () => session.sessionId
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|