codebakers 1.0.45
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/.vscodeignore +18 -0
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/codebakers-1.0.0.vsix +0 -0
- package/codebakers-1.0.10.vsix +0 -0
- package/codebakers-1.0.11.vsix +0 -0
- package/codebakers-1.0.12.vsix +0 -0
- package/codebakers-1.0.13.vsix +0 -0
- package/codebakers-1.0.14.vsix +0 -0
- package/codebakers-1.0.15.vsix +0 -0
- package/codebakers-1.0.16.vsix +0 -0
- package/codebakers-1.0.17.vsix +0 -0
- package/codebakers-1.0.18.vsix +0 -0
- package/codebakers-1.0.19.vsix +0 -0
- package/codebakers-1.0.20.vsix +0 -0
- package/codebakers-1.0.21.vsix +0 -0
- package/codebakers-1.0.22.vsix +0 -0
- package/codebakers-1.0.23.vsix +0 -0
- package/codebakers-1.0.24.vsix +0 -0
- package/codebakers-1.0.25.vsix +0 -0
- package/codebakers-1.0.26.vsix +0 -0
- package/codebakers-1.0.27.vsix +0 -0
- package/codebakers-1.0.28.vsix +0 -0
- package/codebakers-1.0.29.vsix +0 -0
- package/codebakers-1.0.30.vsix +0 -0
- package/codebakers-1.0.31.vsix +0 -0
- package/codebakers-1.0.32.vsix +0 -0
- package/codebakers-1.0.35.vsix +0 -0
- package/codebakers-1.0.36.vsix +0 -0
- package/codebakers-1.0.37.vsix +0 -0
- package/codebakers-1.0.38.vsix +0 -0
- package/codebakers-1.0.39.vsix +0 -0
- package/codebakers-1.0.40.vsix +0 -0
- package/codebakers-1.0.41.vsix +0 -0
- package/codebakers-1.0.42.vsix +0 -0
- package/codebakers-1.0.43.vsix +0 -0
- package/codebakers-1.0.44.vsix +0 -0
- package/codebakers-1.0.45.vsix +0 -0
- package/dist/extension.js +1394 -0
- package/esbuild.js +63 -0
- package/media/icon.png +0 -0
- package/media/icon.svg +7 -0
- package/nul +1 -0
- package/package.json +127 -0
- package/preview.html +547 -0
- package/src/ChatPanelProvider.ts +1815 -0
- package/src/ChatViewProvider.ts +749 -0
- package/src/CodeBakersClient.ts +1146 -0
- package/src/CodeValidator.ts +645 -0
- package/src/FileOperations.ts +410 -0
- package/src/ProjectContext.ts +526 -0
- package/src/extension.ts +332 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1815 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import { CodeBakersClient, FileOperation, CommandToRun } from './CodeBakersClient';
|
|
3
|
+
import { ProjectContext } from './ProjectContext';
|
|
4
|
+
import { FileOperations } from './FileOperations';
|
|
5
|
+
import { CodeValidator } from './CodeValidator';
|
|
6
|
+
|
|
7
|
+
interface Message {
|
|
8
|
+
role: 'user' | 'assistant';
|
|
9
|
+
content: string;
|
|
10
|
+
thinking?: string;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PendingChange {
|
|
15
|
+
id: string;
|
|
16
|
+
operation: FileOperation;
|
|
17
|
+
status: 'pending' | 'applied' | 'rejected';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PendingCommand {
|
|
21
|
+
id: string;
|
|
22
|
+
command: CommandToRun;
|
|
23
|
+
status: 'pending' | 'running' | 'done';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ChatPanelProvider {
|
|
27
|
+
private static _instance: ChatPanelProvider | undefined;
|
|
28
|
+
private _panel: vscode.WebviewPanel | undefined;
|
|
29
|
+
private _messages: Message[] = [];
|
|
30
|
+
private _conversationSummary: string = '';
|
|
31
|
+
private readonly fileOps: FileOperations;
|
|
32
|
+
private _abortController: AbortController | null = null;
|
|
33
|
+
|
|
34
|
+
// Separate tracking for pending operations (Claude Code style)
|
|
35
|
+
private _pendingChanges: PendingChange[] = [];
|
|
36
|
+
private _pendingCommands: PendingCommand[] = [];
|
|
37
|
+
|
|
38
|
+
// Throttling for streaming updates
|
|
39
|
+
private _streamBuffer: string = '';
|
|
40
|
+
private _thinkingBuffer: string = '';
|
|
41
|
+
private _streamThrottleTimer: NodeJS.Timeout | null = null;
|
|
42
|
+
private _lastStreamUpdate: number = 0;
|
|
43
|
+
private readonly STREAM_THROTTLE_MS = 50; // Update UI every 50ms max
|
|
44
|
+
|
|
45
|
+
private constructor(
|
|
46
|
+
private readonly context: vscode.ExtensionContext,
|
|
47
|
+
private readonly client: CodeBakersClient,
|
|
48
|
+
private readonly projectContext: ProjectContext
|
|
49
|
+
) {
|
|
50
|
+
this.fileOps = new FileOperations();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static getInstance(
|
|
54
|
+
context: vscode.ExtensionContext,
|
|
55
|
+
client: CodeBakersClient,
|
|
56
|
+
projectContext: ProjectContext
|
|
57
|
+
): ChatPanelProvider {
|
|
58
|
+
if (!ChatPanelProvider._instance) {
|
|
59
|
+
ChatPanelProvider._instance = new ChatPanelProvider(context, client, projectContext);
|
|
60
|
+
}
|
|
61
|
+
return ChatPanelProvider._instance;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public show() {
|
|
65
|
+
if (this._panel) {
|
|
66
|
+
this._panel.reveal(vscode.ViewColumn.Beside);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this._panel = vscode.window.createWebviewPanel(
|
|
71
|
+
'codebakers.chat',
|
|
72
|
+
'CodeBakers',
|
|
73
|
+
{
|
|
74
|
+
viewColumn: vscode.ViewColumn.Beside,
|
|
75
|
+
preserveFocus: true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
enableScripts: true,
|
|
79
|
+
retainContextWhenHidden: true,
|
|
80
|
+
localResourceRoots: [this.context.extensionUri]
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
this._panel.iconPath = vscode.Uri.joinPath(this.context.extensionUri, 'media', 'icon.svg');
|
|
85
|
+
this._panel.webview.html = this._getHtmlForWebview(this._panel.webview);
|
|
86
|
+
|
|
87
|
+
this._panel.webview.onDidReceiveMessage(async (data) => {
|
|
88
|
+
switch (data.type) {
|
|
89
|
+
case 'sendMessage':
|
|
90
|
+
await this.sendMessage(data.message);
|
|
91
|
+
break;
|
|
92
|
+
case 'clearChat':
|
|
93
|
+
this._messages = [];
|
|
94
|
+
this._conversationSummary = '';
|
|
95
|
+
this._pendingChanges = [];
|
|
96
|
+
this._pendingCommands = [];
|
|
97
|
+
this._updateWebview();
|
|
98
|
+
break;
|
|
99
|
+
case 'runTool':
|
|
100
|
+
await this._executeTool(data.tool);
|
|
101
|
+
break;
|
|
102
|
+
case 'login':
|
|
103
|
+
await this.client.login();
|
|
104
|
+
break;
|
|
105
|
+
case 'applyFile':
|
|
106
|
+
await this._applyFileOperation(data.id);
|
|
107
|
+
break;
|
|
108
|
+
case 'applyAllFiles':
|
|
109
|
+
await this._applyAllPendingChanges();
|
|
110
|
+
break;
|
|
111
|
+
case 'rejectFile':
|
|
112
|
+
this._rejectFileOperation(data.id);
|
|
113
|
+
break;
|
|
114
|
+
case 'rejectAllFiles':
|
|
115
|
+
this._rejectAllPendingChanges();
|
|
116
|
+
break;
|
|
117
|
+
case 'runCommand':
|
|
118
|
+
await this._runCommand(data.id);
|
|
119
|
+
break;
|
|
120
|
+
case 'showDiff':
|
|
121
|
+
await this._showDiff(data.id);
|
|
122
|
+
break;
|
|
123
|
+
case 'undoFile':
|
|
124
|
+
await this._undoFileOperation(data.id);
|
|
125
|
+
break;
|
|
126
|
+
case 'cancelRequest':
|
|
127
|
+
this._cancelCurrentRequest();
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this._panel.onDidDispose(() => {
|
|
133
|
+
this._panel = undefined;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this._initializeStatus();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public refresh() {
|
|
140
|
+
if (this._panel) {
|
|
141
|
+
this._initializeStatus();
|
|
142
|
+
this._updateWebview();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async _initializeStatus() {
|
|
147
|
+
if (!this._panel) return;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const planInfo = this.client.getPlanInfo();
|
|
151
|
+
this._panel.webview.postMessage({
|
|
152
|
+
type: 'updatePlan',
|
|
153
|
+
plan: planInfo.plan
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!this.client.hasSessionToken()) {
|
|
157
|
+
this._panel.webview.postMessage({
|
|
158
|
+
type: 'updateHealth',
|
|
159
|
+
health: 0,
|
|
160
|
+
score: 0
|
|
161
|
+
});
|
|
162
|
+
this._panel.webview.postMessage({
|
|
163
|
+
type: 'showLogin'
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const health = await this.client.guardianStatus();
|
|
170
|
+
this._panel.webview.postMessage({
|
|
171
|
+
type: 'updateHealth',
|
|
172
|
+
health: health.data?.health || 85,
|
|
173
|
+
score: health.data?.health || 85
|
|
174
|
+
});
|
|
175
|
+
} catch (healthError) {
|
|
176
|
+
console.warn('Health check failed:', healthError);
|
|
177
|
+
this._panel.webview.postMessage({
|
|
178
|
+
type: 'updateHealth',
|
|
179
|
+
health: 85,
|
|
180
|
+
score: 85
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Failed to initialize status:', error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async _executeTool(toolName: string) {
|
|
189
|
+
if (!this._panel) return;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
this._panel.webview.postMessage({ type: 'typing', isTyping: true });
|
|
193
|
+
const result = await this.client.executeTool(toolName, {});
|
|
194
|
+
|
|
195
|
+
// Add tool result as a message
|
|
196
|
+
this._messages.push({
|
|
197
|
+
role: 'assistant',
|
|
198
|
+
content: `**Tool: ${toolName}**\n\`\`\`json\n${JSON.stringify(result.data || result, null, 2)}\n\`\`\``,
|
|
199
|
+
timestamp: new Date()
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (toolName === 'guardian_status' && result.data?.health) {
|
|
203
|
+
this._panel.webview.postMessage({
|
|
204
|
+
type: 'updateHealth',
|
|
205
|
+
health: result.data.health,
|
|
206
|
+
score: result.data.health
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this._messages.push({
|
|
211
|
+
role: 'assistant',
|
|
212
|
+
content: `**Tool Error: ${toolName}**\n${error instanceof Error ? error.message : 'Tool execution failed'}`,
|
|
213
|
+
timestamp: new Date()
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
this._panel?.webview.postMessage({ type: 'typing', isTyping: false });
|
|
217
|
+
this._updateWebview();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async _applyFileOperation(id: string) {
|
|
222
|
+
if (!this._panel) return;
|
|
223
|
+
|
|
224
|
+
const change = this._pendingChanges.find(c => c.id === id);
|
|
225
|
+
if (!change || change.status !== 'pending') return;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const success = await this.fileOps.applyChange({
|
|
229
|
+
path: change.operation.path,
|
|
230
|
+
action: change.operation.action,
|
|
231
|
+
content: change.operation.content,
|
|
232
|
+
description: change.operation.description
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (success) {
|
|
236
|
+
change.status = 'applied';
|
|
237
|
+
vscode.window.showInformationMessage(`✅ ${change.operation.action}: ${change.operation.path}`);
|
|
238
|
+
|
|
239
|
+
// Open the file if not a delete
|
|
240
|
+
if (change.operation.action !== 'delete') {
|
|
241
|
+
await this.fileOps.openFile(change.operation.path);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
throw new Error('Operation failed');
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
vscode.window.showErrorMessage(`❌ Failed: ${change.operation.path} - ${error}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this._updatePendingChanges();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async _applyAllPendingChanges() {
|
|
254
|
+
if (!this._panel) return;
|
|
255
|
+
|
|
256
|
+
const pending = this._pendingChanges.filter(c => c.status === 'pending');
|
|
257
|
+
if (pending.length === 0) {
|
|
258
|
+
vscode.window.showInformationMessage('No pending changes to apply');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Show progress in webview
|
|
263
|
+
const total = pending.length;
|
|
264
|
+
this._panel?.webview.postMessage({
|
|
265
|
+
type: 'showProgress',
|
|
266
|
+
text: `Applying 0/${total} files...`,
|
|
267
|
+
current: 0,
|
|
268
|
+
total,
|
|
269
|
+
show: true
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Apply files in parallel batches of 5 for speed
|
|
273
|
+
const BATCH_SIZE = 5;
|
|
274
|
+
let success = 0;
|
|
275
|
+
let failed = 0;
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < pending.length; i += BATCH_SIZE) {
|
|
278
|
+
const batch = pending.slice(i, i + BATCH_SIZE);
|
|
279
|
+
|
|
280
|
+
const results = await Promise.allSettled(
|
|
281
|
+
batch.map(async (change) => {
|
|
282
|
+
try {
|
|
283
|
+
const result = await this.fileOps.applyChange({
|
|
284
|
+
path: change.operation.path,
|
|
285
|
+
action: change.operation.action,
|
|
286
|
+
content: change.operation.content,
|
|
287
|
+
description: change.operation.description
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (result) {
|
|
291
|
+
change.status = 'applied';
|
|
292
|
+
return true;
|
|
293
|
+
} else {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(`Failed to apply ${change.operation.path}:`, error);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Count results
|
|
304
|
+
results.forEach((result) => {
|
|
305
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
306
|
+
success++;
|
|
307
|
+
} else {
|
|
308
|
+
failed++;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Update progress indicator
|
|
313
|
+
const done = success + failed;
|
|
314
|
+
this._panel?.webview.postMessage({
|
|
315
|
+
type: 'showProgress',
|
|
316
|
+
text: `Applying ${done}/${total} files...`,
|
|
317
|
+
current: done,
|
|
318
|
+
total,
|
|
319
|
+
show: true
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Update UI after each batch
|
|
323
|
+
this._updatePendingChanges();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this._panel?.webview.postMessage({ type: 'showProgress', show: false });
|
|
327
|
+
vscode.window.showInformationMessage(
|
|
328
|
+
`✅ Applied ${success} file(s)${failed > 0 ? `, ${failed} failed` : ''}`
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Auto-run TypeScript check after applying files
|
|
332
|
+
if (success > 0) {
|
|
333
|
+
await this._runTscCheck();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async _runTscCheck(autoRetry: boolean = true) {
|
|
338
|
+
// Check if project has TypeScript
|
|
339
|
+
const tsconfigExists = await this.fileOps.fileExists('tsconfig.json');
|
|
340
|
+
if (!tsconfigExists) {
|
|
341
|
+
return; // No TypeScript in project
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this._panel?.webview.postMessage({
|
|
345
|
+
type: 'showStatus',
|
|
346
|
+
text: 'Checking TypeScript...',
|
|
347
|
+
show: true
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const validator = new CodeValidator();
|
|
352
|
+
const result = await validator.runTypeScriptCheck();
|
|
353
|
+
|
|
354
|
+
this._panel?.webview.postMessage({ type: 'showStatus', show: false });
|
|
355
|
+
|
|
356
|
+
if (!result.passed && result.errors.length > 0) {
|
|
357
|
+
const action = await vscode.window.showWarningMessage(
|
|
358
|
+
`⚠️ TypeScript errors found (${result.errorCount})`,
|
|
359
|
+
'Auto-Fix with AI',
|
|
360
|
+
'Show Errors',
|
|
361
|
+
'Ignore'
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (action === 'Auto-Fix with AI' && autoRetry) {
|
|
365
|
+
// Send errors to AI for auto-fix
|
|
366
|
+
const errorMessage = `Please fix these TypeScript errors:\n\n${result.errors.map((e: any) =>
|
|
367
|
+
`${e.file}:${e.line}: ${e.message}`
|
|
368
|
+
).join('\n')}`;
|
|
369
|
+
|
|
370
|
+
this._panel?.webview.postMessage({
|
|
371
|
+
type: 'showStatus',
|
|
372
|
+
text: 'AI fixing TypeScript errors...',
|
|
373
|
+
show: true
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Trigger a new AI request with the errors
|
|
377
|
+
await this.sendMessage(errorMessage);
|
|
378
|
+
} else if (action === 'Show Errors') {
|
|
379
|
+
// Show errors in output channel
|
|
380
|
+
const outputChannel = vscode.window.createOutputChannel('CodeBakers TSC');
|
|
381
|
+
outputChannel.clear();
|
|
382
|
+
outputChannel.appendLine('TypeScript Errors:');
|
|
383
|
+
outputChannel.appendLine('=================\n');
|
|
384
|
+
result.errors.forEach((e: any) => {
|
|
385
|
+
outputChannel.appendLine(`${e.file}:${e.line}:${e.column}`);
|
|
386
|
+
outputChannel.appendLine(` ${e.message}\n`);
|
|
387
|
+
});
|
|
388
|
+
outputChannel.show();
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
vscode.window.showInformationMessage('✅ TypeScript check passed!');
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
this._panel?.webview.postMessage({ type: 'showStatus', show: false });
|
|
395
|
+
console.error('TSC check failed:', error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private _rejectFileOperation(id: string) {
|
|
400
|
+
const change = this._pendingChanges.find(c => c.id === id);
|
|
401
|
+
if (change && change.status === 'pending') {
|
|
402
|
+
change.status = 'rejected';
|
|
403
|
+
this._updatePendingChanges();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private _rejectAllPendingChanges() {
|
|
408
|
+
for (const change of this._pendingChanges) {
|
|
409
|
+
if (change.status === 'pending') {
|
|
410
|
+
change.status = 'rejected';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
this._updatePendingChanges();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private async _runCommand(id: string) {
|
|
417
|
+
if (!this._panel) return;
|
|
418
|
+
|
|
419
|
+
const cmd = this._pendingCommands.find(c => c.id === id);
|
|
420
|
+
if (!cmd || cmd.status !== 'pending') return;
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
cmd.status = 'running';
|
|
424
|
+
this._updatePendingChanges();
|
|
425
|
+
|
|
426
|
+
await this.fileOps.runCommand(cmd.command.command, cmd.command.description || 'CodeBakers');
|
|
427
|
+
cmd.status = 'done';
|
|
428
|
+
vscode.window.showInformationMessage(`🚀 Running: ${cmd.command.command}`);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
cmd.status = 'pending'; // Reset to allow retry
|
|
431
|
+
vscode.window.showErrorMessage(`❌ Failed to run command: ${error}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this._updatePendingChanges();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private async _showDiff(id: string) {
|
|
438
|
+
const change = this._pendingChanges.find(c => c.id === id);
|
|
439
|
+
if (!change || !change.operation.content) return;
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
await this.fileOps.showDiff(
|
|
443
|
+
change.operation.path,
|
|
444
|
+
change.operation.content,
|
|
445
|
+
`CodeBakers: ${change.operation.path}`
|
|
446
|
+
);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
vscode.window.showErrorMessage(`❌ Failed to show diff: ${error}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async _undoFileOperation(id: string) {
|
|
453
|
+
const change = this._pendingChanges.find(c => c.id === id);
|
|
454
|
+
if (!change || change.status !== 'applied') {
|
|
455
|
+
vscode.window.showWarningMessage('Cannot undo - change was not applied');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const success = await this.fileOps.restoreFromBackup(change.operation.path);
|
|
461
|
+
if (success) {
|
|
462
|
+
// Mark as pending again so user can re-apply if desired
|
|
463
|
+
change.status = 'pending';
|
|
464
|
+
this._updatePendingChanges();
|
|
465
|
+
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
vscode.window.showErrorMessage(`❌ Failed to undo: ${error}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private _cancelCurrentRequest() {
|
|
472
|
+
if (this._abortController) {
|
|
473
|
+
this._abortController.abort();
|
|
474
|
+
this._abortController = null;
|
|
475
|
+
this._panel?.webview.postMessage({ type: 'requestCancelled' });
|
|
476
|
+
vscode.window.showInformationMessage('Request cancelled');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Throttled streaming update
|
|
481
|
+
private _throttledStreamUpdate() {
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
|
|
484
|
+
if (now - this._lastStreamUpdate < this.STREAM_THROTTLE_MS) {
|
|
485
|
+
// Schedule update if not already scheduled
|
|
486
|
+
if (!this._streamThrottleTimer) {
|
|
487
|
+
this._streamThrottleTimer = setTimeout(() => {
|
|
488
|
+
this._streamThrottleTimer = null;
|
|
489
|
+
this._sendStreamUpdate();
|
|
490
|
+
}, this.STREAM_THROTTLE_MS);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this._sendStreamUpdate();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private _sendStreamUpdate() {
|
|
499
|
+
this._lastStreamUpdate = Date.now();
|
|
500
|
+
|
|
501
|
+
if (this._thinkingBuffer) {
|
|
502
|
+
this._panel?.webview.postMessage({
|
|
503
|
+
type: 'streamThinking',
|
|
504
|
+
thinking: this._thinkingBuffer
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (this._streamBuffer) {
|
|
509
|
+
this._panel?.webview.postMessage({
|
|
510
|
+
type: 'streamContent',
|
|
511
|
+
content: this._streamBuffer
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async sendMessage(userMessage: string) {
|
|
517
|
+
if (!this._panel) return;
|
|
518
|
+
|
|
519
|
+
// Check if user is logged in
|
|
520
|
+
if (!this.client.hasSessionToken()) {
|
|
521
|
+
this._panel.webview.postMessage({ type: 'showLogin' });
|
|
522
|
+
vscode.window.showWarningMessage(
|
|
523
|
+
'Please sign in to CodeBakers first',
|
|
524
|
+
'Sign In with GitHub'
|
|
525
|
+
).then(selection => {
|
|
526
|
+
if (selection === 'Sign In with GitHub') {
|
|
527
|
+
this.client.login();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this._messages.push({
|
|
534
|
+
role: 'user',
|
|
535
|
+
content: userMessage,
|
|
536
|
+
timestamp: new Date()
|
|
537
|
+
});
|
|
538
|
+
this._updateWebview();
|
|
539
|
+
|
|
540
|
+
// Reset stream buffers
|
|
541
|
+
this._streamBuffer = '';
|
|
542
|
+
this._thinkingBuffer = '';
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
this._abortController = new AbortController();
|
|
546
|
+
this._panel.webview.postMessage({ type: 'typing', isTyping: true });
|
|
547
|
+
|
|
548
|
+
const projectState = await this.projectContext.getProjectState();
|
|
549
|
+
const contextualizedMessages = await this._buildContextualizedMessages(userMessage, projectState);
|
|
550
|
+
|
|
551
|
+
const response = await this.client.chat(contextualizedMessages, projectState, {
|
|
552
|
+
onThinking: (thinking) => {
|
|
553
|
+
this._thinkingBuffer = thinking;
|
|
554
|
+
this._throttledStreamUpdate();
|
|
555
|
+
},
|
|
556
|
+
onContent: (content) => {
|
|
557
|
+
this._streamBuffer = content;
|
|
558
|
+
this._throttledStreamUpdate();
|
|
559
|
+
},
|
|
560
|
+
onDone: () => {
|
|
561
|
+
this._panel?.webview.postMessage({ type: 'validating' });
|
|
562
|
+
},
|
|
563
|
+
onError: (error) => {
|
|
564
|
+
this._panel?.webview.postMessage({
|
|
565
|
+
type: 'streamError',
|
|
566
|
+
error: error.message
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
abortSignal: this._abortController.signal
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Add assistant message (content only, no file ops embedded)
|
|
573
|
+
this._messages.push({
|
|
574
|
+
role: 'assistant',
|
|
575
|
+
content: response.content,
|
|
576
|
+
thinking: response.thinking,
|
|
577
|
+
timestamp: new Date()
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Add file operations to pending changes panel
|
|
581
|
+
if (response.fileOperations && response.fileOperations.length > 0) {
|
|
582
|
+
for (const op of response.fileOperations) {
|
|
583
|
+
this._pendingChanges.push({
|
|
584
|
+
id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
585
|
+
operation: op,
|
|
586
|
+
status: 'pending'
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Add commands to pending commands
|
|
592
|
+
if (response.commands && response.commands.length > 0) {
|
|
593
|
+
for (const cmd of response.commands) {
|
|
594
|
+
this._pendingCommands.push({
|
|
595
|
+
id: `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
596
|
+
command: cmd,
|
|
597
|
+
status: 'pending'
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (response.projectUpdates) {
|
|
603
|
+
await this.projectContext.applyUpdates(response.projectUpdates);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (this._messages.length > 20) {
|
|
607
|
+
await this._summarizeConversation();
|
|
608
|
+
}
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if ((error as Error).message !== 'Request was cancelled') {
|
|
611
|
+
this._messages.push({
|
|
612
|
+
role: 'assistant',
|
|
613
|
+
content: `**Error:** ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
614
|
+
timestamp: new Date()
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
// Clear throttle timer
|
|
619
|
+
if (this._streamThrottleTimer) {
|
|
620
|
+
clearTimeout(this._streamThrottleTimer);
|
|
621
|
+
this._streamThrottleTimer = null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
this._abortController = null;
|
|
625
|
+
this._panel?.webview.postMessage({ type: 'typing', isTyping: false });
|
|
626
|
+
this._updateWebview();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private async _buildContextualizedMessages(userMessage: string, projectState: any): Promise<any[]> {
|
|
631
|
+
const messages: any[] = [];
|
|
632
|
+
|
|
633
|
+
if (this._conversationSummary) {
|
|
634
|
+
messages.push({
|
|
635
|
+
role: 'system',
|
|
636
|
+
content: `Previous conversation summary:\n${this._conversationSummary}`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (projectState) {
|
|
641
|
+
messages.push({
|
|
642
|
+
role: 'system',
|
|
643
|
+
content: `Current project state:\n${JSON.stringify(projectState, null, 2)}`
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const recentMessages = this._messages.slice(-10);
|
|
648
|
+
for (const msg of recentMessages) {
|
|
649
|
+
messages.push({
|
|
650
|
+
role: msg.role,
|
|
651
|
+
content: msg.content
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return messages;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private async _summarizeConversation() {
|
|
659
|
+
const oldMessages = this._messages.slice(0, -10);
|
|
660
|
+
if (oldMessages.length === 0) return;
|
|
661
|
+
|
|
662
|
+
const summaryPrompt = `Summarize these conversation messages, keeping key decisions and context:\n${
|
|
663
|
+
oldMessages.map(m => `${m.role}: ${m.content}`).join('\n')
|
|
664
|
+
}`;
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
const summary = await this.client.summarize(summaryPrompt);
|
|
668
|
+
this._conversationSummary = summary;
|
|
669
|
+
this._messages = this._messages.slice(-10);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error('Failed to summarize conversation:', error);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private _updateWebview() {
|
|
676
|
+
if (!this._panel) return;
|
|
677
|
+
|
|
678
|
+
this._panel.webview.postMessage({
|
|
679
|
+
type: 'updateMessages',
|
|
680
|
+
messages: this._messages.map(m => ({
|
|
681
|
+
role: m.role,
|
|
682
|
+
content: m.content,
|
|
683
|
+
thinking: m.thinking,
|
|
684
|
+
timestamp: m.timestamp.toISOString()
|
|
685
|
+
}))
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
this._updatePendingChanges();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private _updatePendingChanges() {
|
|
692
|
+
if (!this._panel) return;
|
|
693
|
+
|
|
694
|
+
const pendingFileChanges = this._pendingChanges.filter(c => c.status === 'pending');
|
|
695
|
+
const pendingCmds = this._pendingCommands.filter(c => c.status === 'pending');
|
|
696
|
+
|
|
697
|
+
this._panel.webview.postMessage({
|
|
698
|
+
type: 'updatePendingChanges',
|
|
699
|
+
changes: this._pendingChanges.map(c => ({
|
|
700
|
+
id: c.id,
|
|
701
|
+
path: c.operation.path,
|
|
702
|
+
action: c.operation.action,
|
|
703
|
+
description: c.operation.description,
|
|
704
|
+
status: c.status,
|
|
705
|
+
hasContent: !!c.operation.content
|
|
706
|
+
})),
|
|
707
|
+
commands: this._pendingCommands.map(c => ({
|
|
708
|
+
id: c.id,
|
|
709
|
+
command: c.command.command,
|
|
710
|
+
description: c.command.description,
|
|
711
|
+
status: c.status
|
|
712
|
+
})),
|
|
713
|
+
pendingCount: pendingFileChanges.length + pendingCmds.length
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private _getHtmlForWebview(webview: vscode.Webview): string {
|
|
718
|
+
return `<!DOCTYPE html>
|
|
719
|
+
<html lang="en">
|
|
720
|
+
<head>
|
|
721
|
+
<meta charset="UTF-8">
|
|
722
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
723
|
+
<title>CodeBakers</title>
|
|
724
|
+
<style>
|
|
725
|
+
* {
|
|
726
|
+
box-sizing: border-box;
|
|
727
|
+
margin: 0;
|
|
728
|
+
padding: 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
body {
|
|
732
|
+
font-family: var(--vscode-font-family);
|
|
733
|
+
font-size: var(--vscode-font-size);
|
|
734
|
+
color: var(--vscode-foreground);
|
|
735
|
+
background: var(--vscode-editor-background);
|
|
736
|
+
height: 100vh;
|
|
737
|
+
display: flex;
|
|
738
|
+
flex-direction: column;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.header {
|
|
742
|
+
padding: 12px 16px;
|
|
743
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
744
|
+
display: flex;
|
|
745
|
+
align-items: center;
|
|
746
|
+
gap: 10px;
|
|
747
|
+
flex-shrink: 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.header-icon {
|
|
751
|
+
width: 20px;
|
|
752
|
+
height: 20px;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.header-title {
|
|
756
|
+
font-weight: 600;
|
|
757
|
+
font-size: 13px;
|
|
758
|
+
flex: 1;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.header-btn {
|
|
762
|
+
background: transparent;
|
|
763
|
+
border: none;
|
|
764
|
+
color: var(--vscode-foreground);
|
|
765
|
+
cursor: pointer;
|
|
766
|
+
padding: 4px 8px;
|
|
767
|
+
border-radius: 4px;
|
|
768
|
+
font-size: 11px;
|
|
769
|
+
opacity: 0.7;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.header-btn:hover {
|
|
773
|
+
background: var(--vscode-toolbar-hoverBackground);
|
|
774
|
+
opacity: 1;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.plan-badge {
|
|
778
|
+
font-size: 10px;
|
|
779
|
+
padding: 2px 8px;
|
|
780
|
+
background: var(--vscode-button-background);
|
|
781
|
+
color: var(--vscode-button-foreground);
|
|
782
|
+
border-radius: 10px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.plan-badge.trial {
|
|
786
|
+
background: #f0a030;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* Main content area */
|
|
790
|
+
.main-content {
|
|
791
|
+
flex: 1;
|
|
792
|
+
display: flex;
|
|
793
|
+
flex-direction: column;
|
|
794
|
+
min-height: 0;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/* Messages area */
|
|
798
|
+
.messages {
|
|
799
|
+
flex: 1;
|
|
800
|
+
overflow-y: auto;
|
|
801
|
+
padding: 16px;
|
|
802
|
+
display: flex;
|
|
803
|
+
flex-direction: column;
|
|
804
|
+
gap: 12px;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.message {
|
|
808
|
+
max-width: 90%;
|
|
809
|
+
padding: 10px 14px;
|
|
810
|
+
border-radius: 10px;
|
|
811
|
+
line-height: 1.5;
|
|
812
|
+
font-size: 13px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.message.user {
|
|
816
|
+
background: var(--vscode-button-background);
|
|
817
|
+
color: var(--vscode-button-foreground);
|
|
818
|
+
align-self: flex-end;
|
|
819
|
+
border-bottom-right-radius: 4px;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.message.assistant {
|
|
823
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
824
|
+
align-self: flex-start;
|
|
825
|
+
border-bottom-left-radius: 4px;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.message pre {
|
|
829
|
+
background: var(--vscode-textCodeBlock-background);
|
|
830
|
+
padding: 8px;
|
|
831
|
+
border-radius: 4px;
|
|
832
|
+
overflow-x: auto;
|
|
833
|
+
margin: 8px 0;
|
|
834
|
+
font-size: 12px;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.message code {
|
|
838
|
+
font-family: var(--vscode-editor-font-family);
|
|
839
|
+
font-size: 12px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.message h1, .message h2, .message h3, .message h4 {
|
|
843
|
+
margin: 10px 0 6px 0;
|
|
844
|
+
font-weight: 600;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.message h1 { font-size: 1.3em; }
|
|
848
|
+
.message h2 { font-size: 1.2em; }
|
|
849
|
+
.message h3 { font-size: 1.1em; }
|
|
850
|
+
|
|
851
|
+
.message ul, .message ol {
|
|
852
|
+
margin: 6px 0;
|
|
853
|
+
padding-left: 18px;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.message li { margin: 3px 0; }
|
|
857
|
+
|
|
858
|
+
.message hr {
|
|
859
|
+
border: none;
|
|
860
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
861
|
+
margin: 10px 0;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.message p { margin: 6px 0; }
|
|
865
|
+
.message p:first-child { margin-top: 0; }
|
|
866
|
+
.message p:last-child { margin-bottom: 0; }
|
|
867
|
+
|
|
868
|
+
.thinking-toggle {
|
|
869
|
+
display: flex;
|
|
870
|
+
align-items: center;
|
|
871
|
+
gap: 6px;
|
|
872
|
+
font-size: 11px;
|
|
873
|
+
color: var(--vscode-textLink-foreground);
|
|
874
|
+
cursor: pointer;
|
|
875
|
+
margin-bottom: 8px;
|
|
876
|
+
padding: 4px 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.thinking-content {
|
|
880
|
+
background: var(--vscode-textBlockQuote-background);
|
|
881
|
+
border-left: 2px solid var(--vscode-textLink-foreground);
|
|
882
|
+
padding: 8px;
|
|
883
|
+
font-size: 11px;
|
|
884
|
+
color: var(--vscode-descriptionForeground);
|
|
885
|
+
margin-bottom: 8px;
|
|
886
|
+
display: none;
|
|
887
|
+
max-height: 200px;
|
|
888
|
+
overflow-y: auto;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.thinking-content.show { display: block; }
|
|
892
|
+
|
|
893
|
+
.streaming-indicator {
|
|
894
|
+
align-self: flex-start;
|
|
895
|
+
display: none;
|
|
896
|
+
padding: 10px 14px;
|
|
897
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
898
|
+
border-radius: 10px;
|
|
899
|
+
border-bottom-left-radius: 4px;
|
|
900
|
+
max-width: 90%;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.streaming-indicator.show { display: block; }
|
|
904
|
+
|
|
905
|
+
.typing-dots {
|
|
906
|
+
display: flex;
|
|
907
|
+
gap: 4px;
|
|
908
|
+
padding: 8px 0;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.typing-dot {
|
|
912
|
+
width: 6px;
|
|
913
|
+
height: 6px;
|
|
914
|
+
background: var(--vscode-foreground);
|
|
915
|
+
border-radius: 50%;
|
|
916
|
+
animation: bounce 1.4s infinite ease-in-out;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
920
|
+
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
921
|
+
|
|
922
|
+
@keyframes bounce {
|
|
923
|
+
0%, 80%, 100% { transform: translateY(0); }
|
|
924
|
+
40% { transform: translateY(-6px); }
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/* Pending Changes Panel - Claude Code Style */
|
|
928
|
+
.pending-panel {
|
|
929
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
930
|
+
background: var(--vscode-sideBar-background);
|
|
931
|
+
max-height: 40vh;
|
|
932
|
+
overflow-y: auto;
|
|
933
|
+
display: none;
|
|
934
|
+
flex-shrink: 0;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.pending-panel.show { display: block; }
|
|
938
|
+
|
|
939
|
+
.pending-header {
|
|
940
|
+
display: flex;
|
|
941
|
+
align-items: center;
|
|
942
|
+
justify-content: space-between;
|
|
943
|
+
padding: 10px 14px;
|
|
944
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
945
|
+
position: sticky;
|
|
946
|
+
top: 0;
|
|
947
|
+
z-index: 10;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.pending-title {
|
|
951
|
+
font-weight: 600;
|
|
952
|
+
font-size: 12px;
|
|
953
|
+
display: flex;
|
|
954
|
+
align-items: center;
|
|
955
|
+
gap: 8px;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.pending-count {
|
|
959
|
+
background: var(--vscode-badge-background);
|
|
960
|
+
color: var(--vscode-badge-foreground);
|
|
961
|
+
padding: 2px 6px;
|
|
962
|
+
border-radius: 10px;
|
|
963
|
+
font-size: 10px;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.pending-actions {
|
|
967
|
+
display: flex;
|
|
968
|
+
gap: 8px;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.accept-all-btn {
|
|
972
|
+
background: #28a745;
|
|
973
|
+
color: white;
|
|
974
|
+
border: none;
|
|
975
|
+
border-radius: 4px;
|
|
976
|
+
padding: 5px 12px;
|
|
977
|
+
font-size: 11px;
|
|
978
|
+
font-weight: 500;
|
|
979
|
+
cursor: pointer;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.accept-all-btn:hover { background: #218838; }
|
|
983
|
+
|
|
984
|
+
.reject-all-btn {
|
|
985
|
+
background: transparent;
|
|
986
|
+
color: var(--vscode-foreground);
|
|
987
|
+
border: 1px solid var(--vscode-panel-border);
|
|
988
|
+
border-radius: 4px;
|
|
989
|
+
padding: 5px 12px;
|
|
990
|
+
font-size: 11px;
|
|
991
|
+
cursor: pointer;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.reject-all-btn:hover {
|
|
995
|
+
background: var(--vscode-toolbar-hoverBackground);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.pending-list {
|
|
999
|
+
padding: 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.pending-item {
|
|
1003
|
+
display: flex;
|
|
1004
|
+
align-items: center;
|
|
1005
|
+
gap: 10px;
|
|
1006
|
+
padding: 8px 14px;
|
|
1007
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.pending-item:last-child { border-bottom: none; }
|
|
1011
|
+
|
|
1012
|
+
.pending-item.applied {
|
|
1013
|
+
opacity: 0.5;
|
|
1014
|
+
background: rgba(40, 167, 69, 0.1);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.pending-item.rejected {
|
|
1018
|
+
opacity: 0.5;
|
|
1019
|
+
text-decoration: line-through;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.pending-icon {
|
|
1023
|
+
width: 18px;
|
|
1024
|
+
text-align: center;
|
|
1025
|
+
font-size: 12px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.pending-info {
|
|
1029
|
+
flex: 1;
|
|
1030
|
+
min-width: 0;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.pending-path {
|
|
1034
|
+
font-family: var(--vscode-editor-font-family);
|
|
1035
|
+
font-size: 12px;
|
|
1036
|
+
color: var(--vscode-textLink-foreground);
|
|
1037
|
+
white-space: nowrap;
|
|
1038
|
+
overflow: hidden;
|
|
1039
|
+
text-overflow: ellipsis;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.pending-desc {
|
|
1043
|
+
font-size: 10px;
|
|
1044
|
+
color: var(--vscode-descriptionForeground);
|
|
1045
|
+
margin-top: 2px;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.pending-item-actions {
|
|
1049
|
+
display: flex;
|
|
1050
|
+
gap: 4px;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.item-btn {
|
|
1054
|
+
background: transparent;
|
|
1055
|
+
border: 1px solid var(--vscode-panel-border);
|
|
1056
|
+
border-radius: 3px;
|
|
1057
|
+
padding: 3px 8px;
|
|
1058
|
+
font-size: 10px;
|
|
1059
|
+
cursor: pointer;
|
|
1060
|
+
color: var(--vscode-foreground);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.item-btn:hover {
|
|
1064
|
+
background: var(--vscode-toolbar-hoverBackground);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.item-btn.accept {
|
|
1068
|
+
background: #28a745;
|
|
1069
|
+
border-color: #28a745;
|
|
1070
|
+
color: white;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.item-btn.accept:hover { background: #218838; }
|
|
1074
|
+
|
|
1075
|
+
.item-btn.reject {
|
|
1076
|
+
color: #dc3545;
|
|
1077
|
+
border-color: #dc3545;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.item-btn.reject:hover {
|
|
1081
|
+
background: rgba(220, 53, 69, 0.1);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.command-item {
|
|
1085
|
+
background: var(--vscode-textCodeBlock-background);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.command-text {
|
|
1089
|
+
font-family: var(--vscode-editor-font-family);
|
|
1090
|
+
font-size: 11px;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/* Input area */
|
|
1094
|
+
.input-area {
|
|
1095
|
+
padding: 12px 16px;
|
|
1096
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
1097
|
+
display: flex;
|
|
1098
|
+
gap: 8px;
|
|
1099
|
+
flex-shrink: 0;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.input-area textarea {
|
|
1103
|
+
flex: 1;
|
|
1104
|
+
background: var(--vscode-input-background);
|
|
1105
|
+
color: var(--vscode-input-foreground);
|
|
1106
|
+
border: 1px solid var(--vscode-input-border);
|
|
1107
|
+
border-radius: 6px;
|
|
1108
|
+
padding: 10px 12px;
|
|
1109
|
+
font-family: inherit;
|
|
1110
|
+
font-size: 13px;
|
|
1111
|
+
resize: none;
|
|
1112
|
+
min-height: 40px;
|
|
1113
|
+
max-height: 120px;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.input-area textarea:focus {
|
|
1117
|
+
outline: none;
|
|
1118
|
+
border-color: var(--vscode-focusBorder);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.input-area textarea:disabled {
|
|
1122
|
+
opacity: 0.6;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.send-btn {
|
|
1126
|
+
background: var(--vscode-button-background);
|
|
1127
|
+
color: var(--vscode-button-foreground);
|
|
1128
|
+
border: none;
|
|
1129
|
+
border-radius: 6px;
|
|
1130
|
+
padding: 0 16px;
|
|
1131
|
+
cursor: pointer;
|
|
1132
|
+
font-weight: 500;
|
|
1133
|
+
font-size: 12px;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.send-btn:hover {
|
|
1137
|
+
background: var(--vscode-button-hoverBackground);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.send-btn:disabled {
|
|
1141
|
+
opacity: 0.5;
|
|
1142
|
+
cursor: not-allowed;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.cancel-btn {
|
|
1146
|
+
background: #dc3545;
|
|
1147
|
+
color: white;
|
|
1148
|
+
border: none;
|
|
1149
|
+
border-radius: 6px;
|
|
1150
|
+
padding: 0 16px;
|
|
1151
|
+
cursor: pointer;
|
|
1152
|
+
font-weight: 500;
|
|
1153
|
+
font-size: 12px;
|
|
1154
|
+
display: none;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.cancel-btn.show { display: block; }
|
|
1158
|
+
.cancel-btn:hover { background: #c82333; }
|
|
1159
|
+
|
|
1160
|
+
/* Welcome screen */
|
|
1161
|
+
.welcome {
|
|
1162
|
+
flex: 1;
|
|
1163
|
+
display: flex;
|
|
1164
|
+
flex-direction: column;
|
|
1165
|
+
align-items: center;
|
|
1166
|
+
justify-content: center;
|
|
1167
|
+
padding: 30px;
|
|
1168
|
+
text-align: center;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.welcome-icon { font-size: 48px; margin-bottom: 16px; }
|
|
1172
|
+
.welcome-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
|
|
1173
|
+
.welcome-text { color: var(--vscode-descriptionForeground); margin-bottom: 20px; max-width: 350px; font-size: 13px; }
|
|
1174
|
+
|
|
1175
|
+
.quick-actions {
|
|
1176
|
+
display: flex;
|
|
1177
|
+
flex-wrap: wrap;
|
|
1178
|
+
gap: 8px;
|
|
1179
|
+
justify-content: center;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.quick-action {
|
|
1183
|
+
background: var(--vscode-button-secondaryBackground);
|
|
1184
|
+
color: var(--vscode-button-secondaryForeground);
|
|
1185
|
+
border: none;
|
|
1186
|
+
border-radius: 16px;
|
|
1187
|
+
padding: 6px 14px;
|
|
1188
|
+
cursor: pointer;
|
|
1189
|
+
font-size: 12px;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.quick-action:hover {
|
|
1193
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/* Login prompt */
|
|
1197
|
+
.login-prompt {
|
|
1198
|
+
flex: 1;
|
|
1199
|
+
display: none;
|
|
1200
|
+
flex-direction: column;
|
|
1201
|
+
align-items: center;
|
|
1202
|
+
justify-content: center;
|
|
1203
|
+
padding: 30px;
|
|
1204
|
+
text-align: center;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.login-prompt.show { display: flex; }
|
|
1208
|
+
|
|
1209
|
+
.login-btn {
|
|
1210
|
+
background: var(--vscode-button-background);
|
|
1211
|
+
color: var(--vscode-button-foreground);
|
|
1212
|
+
border: none;
|
|
1213
|
+
border-radius: 6px;
|
|
1214
|
+
padding: 10px 20px;
|
|
1215
|
+
cursor: pointer;
|
|
1216
|
+
font-size: 13px;
|
|
1217
|
+
font-weight: 500;
|
|
1218
|
+
margin-top: 16px;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.login-btn:hover {
|
|
1222
|
+
background: var(--vscode-button-hoverBackground);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/* Status indicator */
|
|
1226
|
+
.status-indicator {
|
|
1227
|
+
display: none;
|
|
1228
|
+
align-items: center;
|
|
1229
|
+
gap: 8px;
|
|
1230
|
+
padding: 8px 14px;
|
|
1231
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
1232
|
+
font-size: 12px;
|
|
1233
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.status-indicator.show { display: flex; }
|
|
1237
|
+
|
|
1238
|
+
.status-spinner {
|
|
1239
|
+
width: 14px;
|
|
1240
|
+
height: 14px;
|
|
1241
|
+
border: 2px solid var(--vscode-foreground);
|
|
1242
|
+
border-top-color: transparent;
|
|
1243
|
+
border-radius: 50%;
|
|
1244
|
+
animation: spin 1s linear infinite;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
@keyframes spin {
|
|
1248
|
+
to { transform: rotate(360deg); }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/* Progress bar */
|
|
1252
|
+
.progress-bar {
|
|
1253
|
+
width: 100%;
|
|
1254
|
+
height: 4px;
|
|
1255
|
+
background: var(--vscode-progressBar-background, #333);
|
|
1256
|
+
border-radius: 2px;
|
|
1257
|
+
margin-top: 6px;
|
|
1258
|
+
overflow: hidden;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.progress-fill {
|
|
1262
|
+
height: 100%;
|
|
1263
|
+
background: var(--vscode-progressBar-foreground, #0e7ad3);
|
|
1264
|
+
border-radius: 2px;
|
|
1265
|
+
transition: width 0.2s ease;
|
|
1266
|
+
}
|
|
1267
|
+
</style>
|
|
1268
|
+
</head>
|
|
1269
|
+
<body>
|
|
1270
|
+
<div class="header">
|
|
1271
|
+
<svg class="header-icon" viewBox="0 0 24 24" fill="currentColor">
|
|
1272
|
+
<path d="M4 4 L10 12 L4 20 L8 20 L12 14 L16 20 L20 20 L14 12 L20 4 L16 4 L12 10 L8 4 Z"/>
|
|
1273
|
+
</svg>
|
|
1274
|
+
<span class="header-title">CodeBakers</span>
|
|
1275
|
+
<span class="plan-badge" id="planBadge">Pro</span>
|
|
1276
|
+
<button class="header-btn" onclick="clearChat()">Clear</button>
|
|
1277
|
+
</div>
|
|
1278
|
+
|
|
1279
|
+
<div class="login-prompt" id="loginPrompt">
|
|
1280
|
+
<div class="welcome-icon">🔐</div>
|
|
1281
|
+
<div class="welcome-title">Sign in to CodeBakers</div>
|
|
1282
|
+
<div class="welcome-text">Connect with GitHub to start your free trial.</div>
|
|
1283
|
+
<button class="login-btn" onclick="login()">Sign in with GitHub</button>
|
|
1284
|
+
</div>
|
|
1285
|
+
|
|
1286
|
+
<div class="main-content" id="mainContent">
|
|
1287
|
+
<div class="messages" id="messages">
|
|
1288
|
+
<div class="welcome" id="welcome">
|
|
1289
|
+
<div class="welcome-icon">🍪</div>
|
|
1290
|
+
<div class="welcome-title">CodeBakers AI</div>
|
|
1291
|
+
<div class="welcome-text">Production-ready code with AI. Ask me to build features, edit files, or audit your code.</div>
|
|
1292
|
+
<div class="quick-actions">
|
|
1293
|
+
<button class="quick-action" onclick="quickAction('/build')">Build Project</button>
|
|
1294
|
+
<button class="quick-action" onclick="quickAction('/feature')">Add Feature</button>
|
|
1295
|
+
<button class="quick-action" onclick="quickAction('/audit')">Audit Code</button>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
|
|
1299
|
+
<div class="streaming-indicator" id="streaming">
|
|
1300
|
+
<div class="typing-dots">
|
|
1301
|
+
<div class="typing-dot"></div>
|
|
1302
|
+
<div class="typing-dot"></div>
|
|
1303
|
+
<div class="typing-dot"></div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<div id="streamingContent" style="margin-top: 8px; display: none;"></div>
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
|
|
1309
|
+
<!-- Claude Code Style Pending Changes Panel -->
|
|
1310
|
+
<div class="pending-panel" id="pendingPanel">
|
|
1311
|
+
<div class="pending-header">
|
|
1312
|
+
<div class="pending-title">
|
|
1313
|
+
<span>Pending Changes</span>
|
|
1314
|
+
<span class="pending-count" id="pendingCount">0</span>
|
|
1315
|
+
</div>
|
|
1316
|
+
<div class="pending-actions">
|
|
1317
|
+
<button class="reject-all-btn" onclick="rejectAll()">Reject All</button>
|
|
1318
|
+
<button class="accept-all-btn" onclick="acceptAll()">Accept All</button>
|
|
1319
|
+
</div>
|
|
1320
|
+
</div>
|
|
1321
|
+
<div class="pending-list" id="pendingList"></div>
|
|
1322
|
+
</div>
|
|
1323
|
+
|
|
1324
|
+
<div class="status-indicator" id="statusIndicator">
|
|
1325
|
+
<div class="status-spinner"></div>
|
|
1326
|
+
<span id="statusText">Processing...</span>
|
|
1327
|
+
</div>
|
|
1328
|
+
</div>
|
|
1329
|
+
|
|
1330
|
+
<div class="input-area">
|
|
1331
|
+
<textarea
|
|
1332
|
+
id="input"
|
|
1333
|
+
placeholder="Ask CodeBakers anything..."
|
|
1334
|
+
rows="1"
|
|
1335
|
+
onkeydown="handleKeydown(event)"
|
|
1336
|
+
oninput="autoResize(this)"
|
|
1337
|
+
></textarea>
|
|
1338
|
+
<button class="send-btn" id="sendBtn" onclick="sendMessage()">Send</button>
|
|
1339
|
+
<button class="cancel-btn" id="cancelBtn" onclick="cancelRequest()">Cancel</button>
|
|
1340
|
+
</div>
|
|
1341
|
+
|
|
1342
|
+
<script>
|
|
1343
|
+
const vscode = acquireVsCodeApi();
|
|
1344
|
+
const messagesEl = document.getElementById('messages');
|
|
1345
|
+
const welcomeEl = document.getElementById('welcome');
|
|
1346
|
+
const loginPromptEl = document.getElementById('loginPrompt');
|
|
1347
|
+
const mainContentEl = document.getElementById('mainContent');
|
|
1348
|
+
const inputEl = document.getElementById('input');
|
|
1349
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
1350
|
+
const cancelBtn = document.getElementById('cancelBtn');
|
|
1351
|
+
const streamingEl = document.getElementById('streaming');
|
|
1352
|
+
const streamingContentEl = document.getElementById('streamingContent');
|
|
1353
|
+
const pendingPanel = document.getElementById('pendingPanel');
|
|
1354
|
+
const pendingList = document.getElementById('pendingList');
|
|
1355
|
+
const pendingCount = document.getElementById('pendingCount');
|
|
1356
|
+
const statusIndicator = document.getElementById('statusIndicator');
|
|
1357
|
+
const statusText = document.getElementById('statusText');
|
|
1358
|
+
|
|
1359
|
+
let currentMessages = [];
|
|
1360
|
+
let currentChanges = [];
|
|
1361
|
+
let currentCommands = [];
|
|
1362
|
+
let isStreaming = false;
|
|
1363
|
+
|
|
1364
|
+
// Command history for up/down navigation
|
|
1365
|
+
let commandHistory = JSON.parse(localStorage.getItem('codebakers-history') || '[]');
|
|
1366
|
+
let historyIndex = -1;
|
|
1367
|
+
let tempInput = ''; // Store current input when navigating
|
|
1368
|
+
|
|
1369
|
+
function sendMessage() {
|
|
1370
|
+
const message = inputEl.value.trim();
|
|
1371
|
+
if (message) {
|
|
1372
|
+
// Add to history (avoid duplicates of last command)
|
|
1373
|
+
if (commandHistory.length === 0 || commandHistory[commandHistory.length - 1] !== message) {
|
|
1374
|
+
commandHistory.push(message);
|
|
1375
|
+
// Keep only last 50 commands
|
|
1376
|
+
if (commandHistory.length > 50) {
|
|
1377
|
+
commandHistory = commandHistory.slice(-50);
|
|
1378
|
+
}
|
|
1379
|
+
localStorage.setItem('codebakers-history', JSON.stringify(commandHistory));
|
|
1380
|
+
}
|
|
1381
|
+
historyIndex = -1;
|
|
1382
|
+
tempInput = '';
|
|
1383
|
+
}
|
|
1384
|
+
if (!message || isStreaming) return;
|
|
1385
|
+
|
|
1386
|
+
vscode.postMessage({ type: 'sendMessage', message });
|
|
1387
|
+
inputEl.value = '';
|
|
1388
|
+
inputEl.style.height = 'auto';
|
|
1389
|
+
setStreamingState(true);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function cancelRequest() {
|
|
1393
|
+
vscode.postMessage({ type: 'cancelRequest' });
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function setStreamingState(streaming) {
|
|
1397
|
+
isStreaming = streaming;
|
|
1398
|
+
sendBtn.style.display = streaming ? 'none' : 'block';
|
|
1399
|
+
cancelBtn.classList.toggle('show', streaming);
|
|
1400
|
+
inputEl.disabled = streaming;
|
|
1401
|
+
streamingEl.classList.toggle('show', streaming);
|
|
1402
|
+
|
|
1403
|
+
if (streaming) {
|
|
1404
|
+
streamingContentEl.style.display = 'none';
|
|
1405
|
+
streamingContentEl.innerHTML = '';
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function quickAction(command) {
|
|
1410
|
+
inputEl.value = command + ' ';
|
|
1411
|
+
inputEl.focus();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function clearChat() {
|
|
1415
|
+
vscode.postMessage({ type: 'clearChat' });
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function login() {
|
|
1419
|
+
vscode.postMessage({ type: 'login' });
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function acceptAll() {
|
|
1423
|
+
vscode.postMessage({ type: 'applyAllFiles' });
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function rejectAll() {
|
|
1427
|
+
vscode.postMessage({ type: 'rejectAllFiles' });
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function acceptFile(id) {
|
|
1431
|
+
vscode.postMessage({ type: 'applyFile', id });
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function rejectFile(id) {
|
|
1435
|
+
vscode.postMessage({ type: 'rejectFile', id });
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function showDiff(id) {
|
|
1439
|
+
vscode.postMessage({ type: 'showDiff', id });
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function undoFile(id) {
|
|
1443
|
+
vscode.postMessage({ type: 'undoFile', id });
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function runCommand(id) {
|
|
1447
|
+
vscode.postMessage({ type: 'runCommand', id });
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function handleKeydown(e) {
|
|
1451
|
+
// Enter (without Shift) or Ctrl+Enter to send
|
|
1452
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1453
|
+
e.preventDefault();
|
|
1454
|
+
sendMessage();
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Up arrow: Previous command in history
|
|
1459
|
+
if (e.key === 'ArrowUp' && commandHistory.length > 0) {
|
|
1460
|
+
e.preventDefault();
|
|
1461
|
+
if (historyIndex === -1) {
|
|
1462
|
+
tempInput = inputEl.value; // Save current input
|
|
1463
|
+
historyIndex = commandHistory.length - 1;
|
|
1464
|
+
} else if (historyIndex > 0) {
|
|
1465
|
+
historyIndex--;
|
|
1466
|
+
}
|
|
1467
|
+
inputEl.value = commandHistory[historyIndex];
|
|
1468
|
+
autoResize(inputEl);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Down arrow: Next command in history
|
|
1473
|
+
if (e.key === 'ArrowDown' && historyIndex !== -1) {
|
|
1474
|
+
e.preventDefault();
|
|
1475
|
+
if (historyIndex < commandHistory.length - 1) {
|
|
1476
|
+
historyIndex++;
|
|
1477
|
+
inputEl.value = commandHistory[historyIndex];
|
|
1478
|
+
} else {
|
|
1479
|
+
historyIndex = -1;
|
|
1480
|
+
inputEl.value = tempInput; // Restore saved input
|
|
1481
|
+
}
|
|
1482
|
+
autoResize(inputEl);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Global keyboard shortcuts
|
|
1488
|
+
document.addEventListener('keydown', function(e) {
|
|
1489
|
+
// Ctrl+Shift+A: Accept all pending changes
|
|
1490
|
+
if (e.ctrlKey && e.shiftKey && e.key === 'A') {
|
|
1491
|
+
e.preventDefault();
|
|
1492
|
+
acceptAll();
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Escape: Cancel current request
|
|
1497
|
+
if (e.key === 'Escape') {
|
|
1498
|
+
e.preventDefault();
|
|
1499
|
+
cancelRequest();
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Ctrl+Enter anywhere: Focus input and send if has content
|
|
1504
|
+
if (e.ctrlKey && e.key === 'Enter') {
|
|
1505
|
+
e.preventDefault();
|
|
1506
|
+
inputEl.focus();
|
|
1507
|
+
if (inputEl.value.trim()) {
|
|
1508
|
+
sendMessage();
|
|
1509
|
+
}
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Ctrl+/ : Focus input
|
|
1514
|
+
if (e.ctrlKey && e.key === '/') {
|
|
1515
|
+
e.preventDefault();
|
|
1516
|
+
inputEl.focus();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
function autoResize(el) {
|
|
1522
|
+
el.style.height = 'auto';
|
|
1523
|
+
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function escapeHtml(text) {
|
|
1527
|
+
const div = document.createElement('div');
|
|
1528
|
+
div.textContent = text;
|
|
1529
|
+
return div.innerHTML;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function formatContent(content) {
|
|
1533
|
+
if (!content) return '';
|
|
1534
|
+
|
|
1535
|
+
let text = content;
|
|
1536
|
+
const bt = String.fromCharCode(96); // backtick
|
|
1537
|
+
const bt3 = bt + bt + bt;
|
|
1538
|
+
|
|
1539
|
+
// Step 1: Extract and protect code blocks
|
|
1540
|
+
const codeBlocks = [];
|
|
1541
|
+
const codeBlockPattern = bt3 + '([a-zA-Z]*)' + String.fromCharCode(10) + '([\\s\\S]*?)' + bt3;
|
|
1542
|
+
text = text.replace(new RegExp(codeBlockPattern, 'g'), function(match, lang, code) {
|
|
1543
|
+
const idx = codeBlocks.length;
|
|
1544
|
+
codeBlocks.push('<pre class="code-block"><code>' + escapeHtml(code.trim()) + '</code></pre>');
|
|
1545
|
+
return '%%CODE' + idx + '%%';
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
// Step 2: Extract and protect inline code
|
|
1549
|
+
const inlineCodes = [];
|
|
1550
|
+
const inlinePattern = bt + '([^' + bt + ']+)' + bt;
|
|
1551
|
+
text = text.replace(new RegExp(inlinePattern, 'g'), function(match, code) {
|
|
1552
|
+
const idx = inlineCodes.length;
|
|
1553
|
+
inlineCodes.push('<code class="inline-code">' + escapeHtml(code) + '</code>');
|
|
1554
|
+
return '%%INLINE' + idx + '%%';
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// Step 3: Strip markdown to plain text (not converting to HTML tags)
|
|
1558
|
+
// Remove headers markers but keep text
|
|
1559
|
+
text = text.replace(/^#{1,6} /gm, '');
|
|
1560
|
+
|
|
1561
|
+
// Remove bold markers but keep text: **text** -> text
|
|
1562
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
|
|
1563
|
+
|
|
1564
|
+
// Remove italic markers but keep text: *text* -> text
|
|
1565
|
+
text = text.replace(/\*([^*]+)\*/g, '$1');
|
|
1566
|
+
|
|
1567
|
+
// Remove horizontal rules
|
|
1568
|
+
text = text.replace(/^-{3,}$/gm, '');
|
|
1569
|
+
text = text.replace(/^_{3,}$/gm, '');
|
|
1570
|
+
text = text.replace(/^\*{3,}$/gm, '');
|
|
1571
|
+
|
|
1572
|
+
// Clean up list markers but keep text
|
|
1573
|
+
text = text.replace(/^[\-\*] /gm, '• ');
|
|
1574
|
+
text = text.replace(/^\d+\. /gm, '• ');
|
|
1575
|
+
|
|
1576
|
+
// Remove link syntax but show text: [text](url) -> text
|
|
1577
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
1578
|
+
|
|
1579
|
+
// Step 4: Escape HTML in text (but not in code placeholders)
|
|
1580
|
+
// Split by placeholders, escape non-placeholder parts, rejoin
|
|
1581
|
+
const parts = text.split(/(%%CODE\d+%%|%%INLINE\d+%%)/g);
|
|
1582
|
+
text = parts.map(part => {
|
|
1583
|
+
if (part.match(/^%%CODE\d+%%$/) || part.match(/^%%INLINE\d+%%$/)) {
|
|
1584
|
+
return part; // Keep placeholder as-is
|
|
1585
|
+
}
|
|
1586
|
+
return escapeHtml(part);
|
|
1587
|
+
}).join('');
|
|
1588
|
+
|
|
1589
|
+
// Step 5: Restore code blocks and inline code
|
|
1590
|
+
for (let i = 0; i < codeBlocks.length; i++) {
|
|
1591
|
+
text = text.replace('%%CODE' + i + '%%', codeBlocks[i]);
|
|
1592
|
+
}
|
|
1593
|
+
for (let i = 0; i < inlineCodes.length; i++) {
|
|
1594
|
+
text = text.replace('%%INLINE' + i + '%%', inlineCodes[i]);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Step 6: Handle paragraphs and line breaks
|
|
1598
|
+
const paragraphs = text.split(/\n\n+/);
|
|
1599
|
+
const formatted = paragraphs.map(p => {
|
|
1600
|
+
const trimmed = p.trim();
|
|
1601
|
+
if (!trimmed) return '';
|
|
1602
|
+
if (trimmed.startsWith('<pre') || trimmed.startsWith('<code')) return trimmed;
|
|
1603
|
+
return '<p>' + trimmed.replace(/\n/g, '<br>') + '</p>';
|
|
1604
|
+
}).filter(p => p).join('');
|
|
1605
|
+
|
|
1606
|
+
return formatted || '<p></p>';
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function renderMessage(msg, index) {
|
|
1610
|
+
const div = document.createElement('div');
|
|
1611
|
+
div.className = 'message ' + msg.role;
|
|
1612
|
+
|
|
1613
|
+
let html = '';
|
|
1614
|
+
|
|
1615
|
+
// Thinking toggle
|
|
1616
|
+
if (msg.role === 'assistant' && msg.thinking) {
|
|
1617
|
+
html += '<div class="thinking-toggle" onclick="toggleThinking(this)">▶ Show reasoning</div>';
|
|
1618
|
+
html += '<div class="thinking-content">' + escapeHtml(msg.thinking) + '</div>';
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
html += formatContent(msg.content);
|
|
1622
|
+
|
|
1623
|
+
div.innerHTML = html;
|
|
1624
|
+
return div;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function toggleThinking(el) {
|
|
1628
|
+
const content = el.nextElementSibling;
|
|
1629
|
+
const isShown = content.classList.toggle('show');
|
|
1630
|
+
el.textContent = isShown ? '▼ Hide reasoning' : '▶ Show reasoning';
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function renderPendingChanges() {
|
|
1634
|
+
const pending = currentChanges.filter(c => c.status === 'pending');
|
|
1635
|
+
const pendingCmds = currentCommands.filter(c => c.status === 'pending');
|
|
1636
|
+
const total = pending.length + pendingCmds.length;
|
|
1637
|
+
|
|
1638
|
+
pendingCount.textContent = total;
|
|
1639
|
+
pendingPanel.classList.toggle('show', currentChanges.length > 0 || currentCommands.length > 0);
|
|
1640
|
+
|
|
1641
|
+
let html = '';
|
|
1642
|
+
|
|
1643
|
+
// File changes
|
|
1644
|
+
for (const change of currentChanges) {
|
|
1645
|
+
const icon = change.action === 'create' ? '➕' : change.action === 'edit' ? '✏️' : '🗑️';
|
|
1646
|
+
const statusClass = change.status !== 'pending' ? change.status : '';
|
|
1647
|
+
|
|
1648
|
+
html += '<div class="pending-item ' + statusClass + '">';
|
|
1649
|
+
html += '<span class="pending-icon">' + icon + '</span>';
|
|
1650
|
+
html += '<div class="pending-info">';
|
|
1651
|
+
html += '<div class="pending-path">' + escapeHtml(change.path) + '</div>';
|
|
1652
|
+
if (change.description) {
|
|
1653
|
+
html += '<div class="pending-desc">' + escapeHtml(change.description) + '</div>';
|
|
1654
|
+
}
|
|
1655
|
+
html += '</div>';
|
|
1656
|
+
|
|
1657
|
+
if (change.status === 'pending') {
|
|
1658
|
+
html += '<div class="pending-item-actions">';
|
|
1659
|
+
if (change.hasContent && change.action !== 'delete') {
|
|
1660
|
+
html += '<button class="item-btn" onclick="showDiff(\'' + change.id + '\')">Diff</button>';
|
|
1661
|
+
}
|
|
1662
|
+
html += '<button class="item-btn reject" onclick="rejectFile(\'' + change.id + '\')">✕</button>';
|
|
1663
|
+
html += '<button class="item-btn accept" onclick="acceptFile(\'' + change.id + '\')">✓</button>';
|
|
1664
|
+
html += '</div>';
|
|
1665
|
+
} else if (change.status === 'applied') {
|
|
1666
|
+
html += '<div class="pending-item-actions">';
|
|
1667
|
+
html += '<span style="font-size: 10px; color: #28a745; margin-right: 6px;">✓ applied</span>';
|
|
1668
|
+
html += '<button class="item-btn" onclick="undoFile(\'' + change.id + '\')" title="Undo this change">↩</button>';
|
|
1669
|
+
html += '</div>';
|
|
1670
|
+
} else {
|
|
1671
|
+
html += '<span style="font-size: 10px; opacity: 0.7;">' + change.status + '</span>';
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
html += '</div>';
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Commands
|
|
1678
|
+
for (const cmd of currentCommands) {
|
|
1679
|
+
const statusClass = cmd.status !== 'pending' ? cmd.status : '';
|
|
1680
|
+
|
|
1681
|
+
html += '<div class="pending-item command-item ' + statusClass + '">';
|
|
1682
|
+
html += '<span class="pending-icon">▶</span>';
|
|
1683
|
+
html += '<div class="pending-info">';
|
|
1684
|
+
html += '<div class="command-text">' + escapeHtml(cmd.command) + '</div>';
|
|
1685
|
+
if (cmd.description) {
|
|
1686
|
+
html += '<div class="pending-desc">' + escapeHtml(cmd.description) + '</div>';
|
|
1687
|
+
}
|
|
1688
|
+
html += '</div>';
|
|
1689
|
+
|
|
1690
|
+
if (cmd.status === 'pending') {
|
|
1691
|
+
html += '<div class="pending-item-actions">';
|
|
1692
|
+
html += '<button class="item-btn accept" onclick="runCommand(\'' + cmd.id + '\')">Run</button>';
|
|
1693
|
+
html += '</div>';
|
|
1694
|
+
} else {
|
|
1695
|
+
html += '<span style="font-size: 10px; opacity: 0.7;">' + cmd.status + '</span>';
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
html += '</div>';
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
pendingList.innerHTML = html;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
window.addEventListener('message', event => {
|
|
1705
|
+
const data = event.data;
|
|
1706
|
+
|
|
1707
|
+
switch (data.type) {
|
|
1708
|
+
case 'updateMessages':
|
|
1709
|
+
currentMessages = data.messages;
|
|
1710
|
+
welcomeEl.style.display = data.messages.length > 0 ? 'none' : 'flex';
|
|
1711
|
+
|
|
1712
|
+
// Clear and re-render messages
|
|
1713
|
+
const existing = messagesEl.querySelectorAll('.message');
|
|
1714
|
+
existing.forEach(el => el.remove());
|
|
1715
|
+
|
|
1716
|
+
data.messages.forEach((msg, i) => {
|
|
1717
|
+
messagesEl.insertBefore(renderMessage(msg, i), streamingEl);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1721
|
+
setStreamingState(false);
|
|
1722
|
+
break;
|
|
1723
|
+
|
|
1724
|
+
case 'updatePendingChanges':
|
|
1725
|
+
currentChanges = data.changes || [];
|
|
1726
|
+
currentCommands = data.commands || [];
|
|
1727
|
+
renderPendingChanges();
|
|
1728
|
+
break;
|
|
1729
|
+
|
|
1730
|
+
case 'typing':
|
|
1731
|
+
if (data.isTyping) {
|
|
1732
|
+
welcomeEl.style.display = 'none';
|
|
1733
|
+
}
|
|
1734
|
+
streamingEl.classList.toggle('show', data.isTyping);
|
|
1735
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1736
|
+
break;
|
|
1737
|
+
|
|
1738
|
+
case 'streamThinking':
|
|
1739
|
+
// Could show thinking indicator if desired
|
|
1740
|
+
break;
|
|
1741
|
+
|
|
1742
|
+
case 'streamContent':
|
|
1743
|
+
streamingContentEl.style.display = 'block';
|
|
1744
|
+
streamingContentEl.innerHTML = formatContent(data.content);
|
|
1745
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1746
|
+
break;
|
|
1747
|
+
|
|
1748
|
+
case 'validating':
|
|
1749
|
+
statusText.textContent = 'Validating TypeScript...';
|
|
1750
|
+
statusIndicator.classList.add('show');
|
|
1751
|
+
break;
|
|
1752
|
+
|
|
1753
|
+
case 'streamError':
|
|
1754
|
+
statusIndicator.classList.remove('show');
|
|
1755
|
+
setStreamingState(false);
|
|
1756
|
+
alert('Error: ' + (data.error || 'Unknown error'));
|
|
1757
|
+
break;
|
|
1758
|
+
|
|
1759
|
+
case 'requestCancelled':
|
|
1760
|
+
statusIndicator.classList.remove('show');
|
|
1761
|
+
setStreamingState(false);
|
|
1762
|
+
break;
|
|
1763
|
+
|
|
1764
|
+
case 'updatePlan':
|
|
1765
|
+
const badge = document.getElementById('planBadge');
|
|
1766
|
+
badge.textContent = data.plan.charAt(0).toUpperCase() + data.plan.slice(1);
|
|
1767
|
+
badge.className = 'plan-badge' + (data.plan === 'trial' ? ' trial' : '');
|
|
1768
|
+
break;
|
|
1769
|
+
|
|
1770
|
+
case 'showStatus':
|
|
1771
|
+
if (data.show) {
|
|
1772
|
+
statusText.textContent = data.text || 'Processing...';
|
|
1773
|
+
statusIndicator.classList.add('show');
|
|
1774
|
+
} else {
|
|
1775
|
+
statusIndicator.classList.remove('show');
|
|
1776
|
+
}
|
|
1777
|
+
break;
|
|
1778
|
+
|
|
1779
|
+
case 'showProgress':
|
|
1780
|
+
if (data.show) {
|
|
1781
|
+
const pct = data.total > 0 ? Math.round((data.current / data.total) * 100) : 0;
|
|
1782
|
+
statusText.innerHTML = data.text + '<div class="progress-bar"><div class="progress-fill" style="width:' + pct + '%"></div></div>';
|
|
1783
|
+
statusIndicator.classList.add('show');
|
|
1784
|
+
} else {
|
|
1785
|
+
statusIndicator.classList.remove('show');
|
|
1786
|
+
}
|
|
1787
|
+
break;
|
|
1788
|
+
|
|
1789
|
+
case 'showLogin':
|
|
1790
|
+
loginPromptEl.classList.add('show');
|
|
1791
|
+
mainContentEl.style.display = 'none';
|
|
1792
|
+
break;
|
|
1793
|
+
|
|
1794
|
+
case 'hideLogin':
|
|
1795
|
+
loginPromptEl.classList.remove('show');
|
|
1796
|
+
mainContentEl.style.display = 'flex';
|
|
1797
|
+
break;
|
|
1798
|
+
|
|
1799
|
+
case 'updateHealth':
|
|
1800
|
+
// Could show health indicator
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
// Hide status after a delay
|
|
1806
|
+
setInterval(() => {
|
|
1807
|
+
if (!isStreaming) {
|
|
1808
|
+
statusIndicator.classList.remove('show');
|
|
1809
|
+
}
|
|
1810
|
+
}, 3000);
|
|
1811
|
+
</script>
|
|
1812
|
+
</body>
|
|
1813
|
+
</html>`;
|
|
1814
|
+
}
|
|
1815
|
+
}
|