xibecode 1.0.1 → 1.0.3
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/dist/commands/chat.d.ts +0 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +18 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/run-pr.d.ts.map +1 -1
- package/dist/commands/run-pr.js +3 -1
- package/dist/commands/run-pr.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +3 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/core/mcp-client.d.ts.map +1 -1
- package/dist/core/mcp-client.js +3 -1
- package/dist/core/mcp-client.js.map +1 -1
- package/dist/core/modes.d.ts.map +1 -1
- package/dist/core/modes.js +6 -0
- package/dist/core/modes.js.map +1 -1
- package/dist/core/plugins.d.ts.map +1 -1
- package/dist/core/plugins.js +3 -1
- package/dist/core/plugins.js.map +1 -1
- package/dist/index.js +2 -39
- package/dist/index.js.map +1 -1
- package/dist/ui/claude-style-chat.d.ts.map +1 -1
- package/dist/ui/claude-style-chat.js +36 -2
- package/dist/ui/claude-style-chat.js.map +1 -1
- package/dist/utils/config.d.ts +14 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +17 -1
- package/dist/utils/config.js.map +1 -1
- package/package.json +14 -36
- package/dist/webui/server.d.ts +0 -99
- package/dist/webui/server.d.ts.map +0 -1
- package/dist/webui/server.js +0 -2608
- package/dist/webui/server.js.map +0 -1
- package/webui-dist/assets/index-CSla6Lzy.css +0 -32
- package/webui-dist/assets/index-jeWUzIG0.js +0 -457
- package/webui-dist/assets/index-jeWUzIG0.js.map +0 -1
- package/webui-dist/assets/xterm-B4aZdZLt.js +0 -10
- package/webui-dist/assets/xterm-B4aZdZLt.js.map +0 -1
- package/webui-dist/assets/xterm-addon-fit-CY2T_uda.js +0 -2
- package/webui-dist/assets/xterm-addon-fit-CY2T_uda.js.map +0 -1
- package/webui-dist/assets/xterm-addon-web-links-D93WX0PV.js +0 -2
- package/webui-dist/assets/xterm-addon-web-links-D93WX0PV.js.map +0 -1
- package/webui-dist/index.html +0 -15
package/dist/webui/server.js
DELETED
|
@@ -1,2608 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XibeCode WebUI Server
|
|
3
|
-
*
|
|
4
|
-
* A lightweight HTTP server that provides:
|
|
5
|
-
* - REST API for interacting with XibeCode
|
|
6
|
-
* - WebSocket for real-time agent communication
|
|
7
|
-
* - Static file serving for the WebUI frontend
|
|
8
|
-
*
|
|
9
|
-
* @module webui/server
|
|
10
|
-
* @since 0.4.0
|
|
11
|
-
*/
|
|
12
|
-
import { createServer } from 'http';
|
|
13
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
14
|
-
import * as fs from 'fs/promises';
|
|
15
|
-
import * as fsSync from 'fs';
|
|
16
|
-
import * as path from 'path';
|
|
17
|
-
import { fileURLToPath } from 'url';
|
|
18
|
-
import { spawn } from 'child_process';
|
|
19
|
-
import { ConfigManager, PROVIDER_CONFIGS } from '../utils/config.js';
|
|
20
|
-
import { SkillManager } from '../core/skills.js';
|
|
21
|
-
import { EnhancedAgent } from '../core/agent.js';
|
|
22
|
-
import { CodingToolExecutor } from '../core/tools.js';
|
|
23
|
-
import { GitUtils } from '../utils/git.js';
|
|
24
|
-
import { TestRunnerDetector } from '../utils/testRunner.js';
|
|
25
|
-
import { TestGenerator, writeTestFile } from '../tools/test-generator.js';
|
|
26
|
-
import { SessionBridge } from '../core/session-bridge.js';
|
|
27
|
-
import { HistoryManager } from '../core/history-manager.js';
|
|
28
|
-
import { extractAtReferences, splitAtReferences } from '../utils/at-references.js';
|
|
29
|
-
import { loadImageAttachment, mimeFromExtension } from '../utils/image-attachments.js';
|
|
30
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
-
const __dirname = path.dirname(__filename);
|
|
32
|
-
/**
|
|
33
|
-
* Available AI models configuration
|
|
34
|
-
*/
|
|
35
|
-
export const AVAILABLE_MODELS = [
|
|
36
|
-
// OpenAI Models
|
|
37
|
-
// GPT-5 Series
|
|
38
|
-
{ id: 'gpt-5.2', name: 'GPT-5.2', provider: 'openai', tier: 'premium' },
|
|
39
|
-
{ id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro', provider: 'openai', tier: 'premium' },
|
|
40
|
-
{ id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex', provider: 'openai', tier: 'premium' },
|
|
41
|
-
{ id: 'gpt-5.1', name: 'GPT-5.1', provider: 'openai', tier: 'standard' },
|
|
42
|
-
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex', provider: 'openai', tier: 'standard' },
|
|
43
|
-
{ id: 'gpt-5.1-chat', name: 'GPT-5.1 Chat', provider: 'openai', tier: 'standard' },
|
|
44
|
-
{ id: 'gpt-5', name: 'GPT-5', provider: 'openai', tier: 'standard' },
|
|
45
|
-
{ id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'openai', tier: 'fast' },
|
|
46
|
-
{ id: 'gpt-5-nano', name: 'GPT-5 Nano', provider: 'openai', tier: 'fast' },
|
|
47
|
-
{ id: 'gpt-5-chat', name: 'GPT-5 Chat', provider: 'openai', tier: 'fast' },
|
|
48
|
-
// GPT-4 & Reasoning
|
|
49
|
-
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai', tier: 'standard' },
|
|
50
|
-
{ id: 'o3-deep-research', name: 'O3 Deep Research', provider: 'openai', tier: 'reasoning' },
|
|
51
|
-
{ id: 'o3-pro', name: 'O3 Pro', provider: 'openai', tier: 'reasoning' },
|
|
52
|
-
{ id: 'o3', name: 'O3', provider: 'openai', tier: 'reasoning' },
|
|
53
|
-
{ id: 'o4-mini', name: 'O4 Mini', provider: 'openai', tier: 'reasoning' },
|
|
54
|
-
{ id: 'o4-mini-deep-research', name: 'O4 Mini Deep Research', provider: 'openai', tier: 'reasoning' },
|
|
55
|
-
// Anthropic Models
|
|
56
|
-
// Claude 4 Series
|
|
57
|
-
{ id: 'claude-opus-4.6', name: 'Claude Opus 4.6', provider: 'anthropic', tier: 'premium' },
|
|
58
|
-
{ id: 'claude-opus-4.5', name: 'Claude Opus 4.5', provider: 'anthropic', tier: 'premium' },
|
|
59
|
-
{ id: 'claude-haiku-4.5', name: 'Claude Haiku 4.5', provider: 'anthropic', tier: 'fast' },
|
|
60
|
-
{ id: 'claude-sonnet-4.5', name: 'Claude Sonnet 4.5', provider: 'anthropic', tier: 'standard' },
|
|
61
|
-
{ id: 'claude-sonnet-4', name: 'Claude Sonnet 4', provider: 'anthropic', tier: 'standard' },
|
|
62
|
-
// Claude 3 Series
|
|
63
|
-
{ id: 'claude-3.7-sonnet', name: 'Claude 3.7 Sonnet', provider: 'anthropic', tier: 'standard' },
|
|
64
|
-
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'anthropic', tier: 'standard' },
|
|
65
|
-
// Google (Native)
|
|
66
|
-
{ id: 'gemini-3-deep-think', name: 'Gemini 3 Deep Think', provider: 'google', tier: 'reasoning' },
|
|
67
|
-
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'google', tier: 'fast' },
|
|
68
|
-
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', provider: 'google', tier: 'premium' },
|
|
69
|
-
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'google', tier: 'premium' },
|
|
70
|
-
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'google', tier: 'fast' },
|
|
71
|
-
// OpenRouter Models
|
|
72
|
-
{ id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet (OpenRouter)', provider: 'openrouter', tier: 'standard' },
|
|
73
|
-
// Zhipu AI (GLM)
|
|
74
|
-
{ id: 'glm-5', name: 'GLM-5', provider: 'zai', tier: 'premium' },
|
|
75
|
-
{ id: 'glm-4.7', name: 'GLM-4.7', provider: 'zai', tier: 'standard' },
|
|
76
|
-
{ id: 'glm-4.6', name: 'GLM-4.6', provider: 'zai', tier: 'standard' },
|
|
77
|
-
{ id: 'glm-4.5-air', name: 'GLM-4.5 Air', provider: 'zai', tier: 'fast' },
|
|
78
|
-
{ id: 'glm-4-plus', name: 'GLM-4 Plus', provider: 'zai', tier: 'standard' },
|
|
79
|
-
// Alibaba (Qwen)
|
|
80
|
-
// Qwen 3 Series
|
|
81
|
-
{ id: 'qwen3.5-coder-plus', name: 'Qwen 3.5 Coder Plus', provider: 'alibaba', tier: 'premium' },
|
|
82
|
-
{ id: 'qwen3.5-max', name: 'Qwen 3.5 Max', provider: 'alibaba', tier: 'premium' },
|
|
83
|
-
{ id: 'qwen3-max-thinking', name: 'Qwen 3 Max Thinking', provider: 'alibaba', tier: 'reasoning' },
|
|
84
|
-
{ id: 'qwen3-coder-plus', name: 'Qwen 3 Coder Plus', provider: 'alibaba', tier: 'standard' },
|
|
85
|
-
{ id: 'qwen3-235b', name: 'Qwen 3 235B', provider: 'alibaba', tier: 'standard' },
|
|
86
|
-
// Qwen 2 Series
|
|
87
|
-
{ id: 'qwen2.5-coder', name: 'Qwen 2.5 Coder', provider: 'alibaba', tier: 'standard' },
|
|
88
|
-
{ id: 'qwen2.5-math', name: 'Qwen 2.5 Math', provider: 'alibaba', tier: 'reasoning' },
|
|
89
|
-
{ id: 'qwen2.5-72b', name: 'Qwen 2.5 72B', provider: 'alibaba', tier: 'standard' },
|
|
90
|
-
// Moonshot (Kimi)
|
|
91
|
-
// Kimi K2 Series
|
|
92
|
-
{ id: 'kimi-k2.5', name: 'Kimi k2.5', provider: 'kimi', tier: 'standard' },
|
|
93
|
-
{ id: 'kimi-k2-thinking', name: 'Kimi k2 Thinking', provider: 'kimi', tier: 'reasoning' },
|
|
94
|
-
{ id: 'kimi-k2-turbo-preview', name: 'Kimi k2 Turbo Preview', provider: 'kimi', tier: 'fast' },
|
|
95
|
-
{ id: 'kimi-k2-0905', name: 'Kimi k2 (0905)', provider: 'kimi', tier: 'standard' },
|
|
96
|
-
{ id: 'kimi-k2-0711', name: 'Kimi k2 (0711)', provider: 'kimi', tier: 'standard' },
|
|
97
|
-
// xAI (Grok)
|
|
98
|
-
// Grok-4 Series
|
|
99
|
-
{ id: 'grok-4.1-fast-reasoning', name: 'Grok 4.1 Fast Reasoning', provider: 'grok', tier: 'reasoning' },
|
|
100
|
-
{ id: 'grok-4.1-fast', name: 'Grok 4.1 Fast', provider: 'grok', tier: 'fast' },
|
|
101
|
-
{ id: 'grok-4', name: 'Grok 4', provider: 'grok', tier: 'premium' },
|
|
102
|
-
{ id: 'grok-4-code', name: 'Grok 4 Code', provider: 'grok', tier: 'standard' },
|
|
103
|
-
{ id: 'grok-4-0709', name: 'Grok 4 (0709)', provider: 'grok', tier: 'standard' },
|
|
104
|
-
// Grok-3 Series
|
|
105
|
-
{ id: 'grok-3', name: 'Grok 3', provider: 'grok', tier: 'standard' },
|
|
106
|
-
{ id: 'grok-3-mini', name: 'Grok 3 Mini', provider: 'grok', tier: 'fast' },
|
|
107
|
-
];
|
|
108
|
-
/**
|
|
109
|
-
* WebUI Server for XibeCode
|
|
110
|
-
*/
|
|
111
|
-
export class WebUIServer {
|
|
112
|
-
server = null;
|
|
113
|
-
wss = null;
|
|
114
|
-
config;
|
|
115
|
-
configManager;
|
|
116
|
-
sessions = new Map();
|
|
117
|
-
wsClients = new Map();
|
|
118
|
-
workingDir;
|
|
119
|
-
historyManager;
|
|
120
|
-
constructor(config = {}) {
|
|
121
|
-
this.config = {
|
|
122
|
-
port: config.port || 3847,
|
|
123
|
-
host: config.host || 'localhost',
|
|
124
|
-
staticDir: config.staticDir || path.join(__dirname, '../../webui-dist'),
|
|
125
|
-
workingDir: config.workingDir || process.cwd(),
|
|
126
|
-
};
|
|
127
|
-
this.workingDir = this.config.workingDir;
|
|
128
|
-
this.configManager = new ConfigManager();
|
|
129
|
-
this.historyManager = new HistoryManager(this.workingDir);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Start the WebUI server
|
|
133
|
-
*/
|
|
134
|
-
async start() {
|
|
135
|
-
return new Promise((resolve, reject) => {
|
|
136
|
-
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
137
|
-
// WebSocket server for real-time communication
|
|
138
|
-
this.wss = new WebSocketServer({ server: this.server });
|
|
139
|
-
this.wss.on('connection', (ws, req) => this.handleWebSocket(ws, req));
|
|
140
|
-
this.server.listen(this.config.port, this.config.host, () => {
|
|
141
|
-
console.log(`XibeCode WebUI running at http://${this.config.host}:${this.config.port}`);
|
|
142
|
-
resolve();
|
|
143
|
-
});
|
|
144
|
-
this.server.on('error', reject);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Stop the WebUI server
|
|
149
|
-
*/
|
|
150
|
-
async stop() {
|
|
151
|
-
return new Promise((resolve) => {
|
|
152
|
-
// Close all WebSocket connections
|
|
153
|
-
this.wsClients.forEach((ws) => ws.close());
|
|
154
|
-
this.wsClients.clear();
|
|
155
|
-
if (this.wss) {
|
|
156
|
-
this.wss.close();
|
|
157
|
-
}
|
|
158
|
-
if (this.server) {
|
|
159
|
-
this.server.close(() => resolve());
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
resolve();
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Handle HTTP requests
|
|
168
|
-
*/
|
|
169
|
-
async handleRequest(req, res) {
|
|
170
|
-
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
171
|
-
const pathname = url.pathname;
|
|
172
|
-
// CORS headers
|
|
173
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
174
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
175
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
176
|
-
if (req.method === 'OPTIONS') {
|
|
177
|
-
res.writeHead(204);
|
|
178
|
-
res.end();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
// API routes
|
|
182
|
-
if (pathname.startsWith('/api/')) {
|
|
183
|
-
await this.handleAPI(req, res, pathname);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
// Static file serving
|
|
187
|
-
await this.serveStatic(req, res, pathname);
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Handle API requests
|
|
191
|
-
*/
|
|
192
|
-
async handleAPI(req, res, pathname) {
|
|
193
|
-
const sendJSON = (data, status = 200) => {
|
|
194
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
195
|
-
res.end(JSON.stringify(data));
|
|
196
|
-
};
|
|
197
|
-
const parseBody = async () => {
|
|
198
|
-
return new Promise((resolve, reject) => {
|
|
199
|
-
let body = '';
|
|
200
|
-
req.on('data', (chunk) => (body += chunk));
|
|
201
|
-
req.on('end', () => {
|
|
202
|
-
try {
|
|
203
|
-
resolve(body ? JSON.parse(body) : {});
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
resolve({});
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
req.on('error', reject);
|
|
210
|
-
});
|
|
211
|
-
};
|
|
212
|
-
try {
|
|
213
|
-
// Health check
|
|
214
|
-
if (pathname === '/api/health') {
|
|
215
|
-
sendJSON({ status: 'ok', version: '1.0.1' });
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
// Configuration
|
|
219
|
-
if (pathname === '/api/config') {
|
|
220
|
-
if (req.method === 'GET') {
|
|
221
|
-
const display = this.configManager.getDisplayConfig();
|
|
222
|
-
const currentModel = this.configManager.getModel();
|
|
223
|
-
const apiKeySet = !!this.configManager.getApiKey();
|
|
224
|
-
const allConfig = this.configManager.getAll();
|
|
225
|
-
sendJSON({
|
|
226
|
-
...display,
|
|
227
|
-
apiKeySet,
|
|
228
|
-
currentModel,
|
|
229
|
-
availableModels: AVAILABLE_MODELS,
|
|
230
|
-
// Raw config values for the settings panel
|
|
231
|
-
raw: {
|
|
232
|
-
provider: allConfig.provider || '',
|
|
233
|
-
model: allConfig.model || 'claude-sonnet-4-5-20250929',
|
|
234
|
-
apiKey: apiKeySet ? '••••••••' : '',
|
|
235
|
-
baseUrl: allConfig.baseUrl || '',
|
|
236
|
-
maxIterations: allConfig.maxIterations ?? 50,
|
|
237
|
-
theme: allConfig.theme || 'default',
|
|
238
|
-
showDetails: this.configManager.getShowDetails(),
|
|
239
|
-
showThinking: this.configManager.getShowThinking(),
|
|
240
|
-
compactThreshold: allConfig.compactThreshold ?? 50000,
|
|
241
|
-
preferredPackageManager: allConfig.preferredPackageManager || 'pnpm',
|
|
242
|
-
enableDryRunByDefault: allConfig.enableDryRunByDefault ?? false,
|
|
243
|
-
gitCheckpointStrategy: allConfig.gitCheckpointStrategy || 'stash',
|
|
244
|
-
testCommandOverride: allConfig.testCommandOverride || '',
|
|
245
|
-
defaultEditor: allConfig.defaultEditor || '',
|
|
246
|
-
statusBarEnabled: allConfig.statusBarEnabled ?? true,
|
|
247
|
-
headerMinimal: allConfig.headerMinimal ?? false,
|
|
248
|
-
sessionDirectory: allConfig.sessionDirectory || '',
|
|
249
|
-
plugins: allConfig.plugins || [],
|
|
250
|
-
},
|
|
251
|
-
providerConfigs: PROVIDER_CONFIGS,
|
|
252
|
-
});
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (req.method === 'PUT') {
|
|
256
|
-
const body = await parseBody();
|
|
257
|
-
// Core AI settings
|
|
258
|
-
if (body.apiKey && !body.apiKey.includes('••••') && !body.apiKey.includes('****')) {
|
|
259
|
-
this.configManager.set('apiKey', body.apiKey);
|
|
260
|
-
}
|
|
261
|
-
if (body.model)
|
|
262
|
-
this.configManager.set('model', body.model);
|
|
263
|
-
if (body.provider !== undefined)
|
|
264
|
-
this.configManager.set('provider', body.provider);
|
|
265
|
-
if (body.baseUrl !== undefined)
|
|
266
|
-
this.configManager.set('baseUrl', body.baseUrl);
|
|
267
|
-
if (body.maxIterations !== undefined)
|
|
268
|
-
this.configManager.set('maxIterations', body.maxIterations);
|
|
269
|
-
// Display settings
|
|
270
|
-
if (body.theme !== undefined)
|
|
271
|
-
this.configManager.set('theme', body.theme);
|
|
272
|
-
if (body.showDetails !== undefined)
|
|
273
|
-
this.configManager.set('showDetails', body.showDetails);
|
|
274
|
-
if (body.showThinking !== undefined)
|
|
275
|
-
this.configManager.set('showThinking', body.showThinking);
|
|
276
|
-
if (body.compactThreshold !== undefined)
|
|
277
|
-
this.configManager.set('compactThreshold', body.compactThreshold);
|
|
278
|
-
// Dev settings
|
|
279
|
-
if (body.preferredPackageManager !== undefined)
|
|
280
|
-
this.configManager.set('preferredPackageManager', body.preferredPackageManager);
|
|
281
|
-
if (body.enableDryRunByDefault !== undefined)
|
|
282
|
-
this.configManager.set('enableDryRunByDefault', body.enableDryRunByDefault);
|
|
283
|
-
if (body.gitCheckpointStrategy !== undefined)
|
|
284
|
-
this.configManager.set('gitCheckpointStrategy', body.gitCheckpointStrategy);
|
|
285
|
-
if (body.testCommandOverride !== undefined)
|
|
286
|
-
this.configManager.set('testCommandOverride', body.testCommandOverride);
|
|
287
|
-
if (body.defaultEditor !== undefined)
|
|
288
|
-
this.configManager.set('defaultEditor', body.defaultEditor);
|
|
289
|
-
if (body.statusBarEnabled !== undefined)
|
|
290
|
-
this.configManager.set('statusBarEnabled', body.statusBarEnabled);
|
|
291
|
-
if (body.headerMinimal !== undefined)
|
|
292
|
-
this.configManager.set('headerMinimal', body.headerMinimal);
|
|
293
|
-
if (body.sessionDirectory !== undefined)
|
|
294
|
-
this.configManager.set('sessionDirectory', body.sessionDirectory);
|
|
295
|
-
sendJSON({ success: true });
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// Models
|
|
300
|
-
if (pathname === '/api/models') {
|
|
301
|
-
sendJSON({
|
|
302
|
-
models: AVAILABLE_MODELS,
|
|
303
|
-
current: this.configManager.getModel(),
|
|
304
|
-
});
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
// MCP servers JSON file (read/write for the Monaco editor)
|
|
308
|
-
if (pathname === '/api/mcp/file') {
|
|
309
|
-
const mcpFilePath = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.xibecode', 'mcp-servers.json');
|
|
310
|
-
if (req.method === 'GET') {
|
|
311
|
-
try {
|
|
312
|
-
const content = await fs.readFile(mcpFilePath, 'utf-8');
|
|
313
|
-
sendJSON({ success: true, content, path: mcpFilePath });
|
|
314
|
-
}
|
|
315
|
-
catch {
|
|
316
|
-
// File doesn't exist yet, return default template
|
|
317
|
-
const defaultContent = JSON.stringify({
|
|
318
|
-
mcpServers: {}
|
|
319
|
-
}, null, 2);
|
|
320
|
-
sendJSON({ success: true, content: defaultContent, path: mcpFilePath });
|
|
321
|
-
}
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (req.method === 'PUT') {
|
|
325
|
-
const body = await parseBody();
|
|
326
|
-
if (!body.content) {
|
|
327
|
-
sendJSON({ success: false, error: 'Missing content' }, 400);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
// Validate it's valid JSON
|
|
332
|
-
JSON.parse(body.content);
|
|
333
|
-
// Ensure directory exists
|
|
334
|
-
const dir = path.dirname(mcpFilePath);
|
|
335
|
-
await fs.mkdir(dir, { recursive: true });
|
|
336
|
-
await fs.writeFile(mcpFilePath, body.content, 'utf-8');
|
|
337
|
-
sendJSON({ success: true });
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
sendJSON({ success: false, error: error.message || 'Invalid JSON' }, 400);
|
|
341
|
-
}
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Project info
|
|
346
|
-
if (pathname === '/api/project') {
|
|
347
|
-
const projectInfo = await this.getProjectInfo();
|
|
348
|
-
sendJSON(projectInfo);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
// Git status
|
|
352
|
-
if (pathname === '/api/git/status') {
|
|
353
|
-
const gitUtils = new GitUtils(this.workingDir);
|
|
354
|
-
try {
|
|
355
|
-
const status = await gitUtils.getStatus();
|
|
356
|
-
sendJSON({ success: true, ...status });
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
360
|
-
}
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
// Git diff
|
|
364
|
-
if (pathname === '/api/git/diff') {
|
|
365
|
-
const gitUtils = new GitUtils(this.workingDir);
|
|
366
|
-
try {
|
|
367
|
-
const diff = await gitUtils.getUnifiedDiff();
|
|
368
|
-
const summary = await gitUtils.getDiffSummary();
|
|
369
|
-
sendJSON({ success: true, diff, summary });
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
// Git log (commit history)
|
|
377
|
-
if (pathname === '/api/git/log') {
|
|
378
|
-
try {
|
|
379
|
-
const { execSync } = await import('child_process');
|
|
380
|
-
const count = 30;
|
|
381
|
-
const logOutput = execSync(`git log --pretty=format:'{"hash":"%H","shortHash":"%h","author":"%an","email":"%ae","date":"%ai","message":"%s","refs":"%D"}' -${count}`, { cwd: this.workingDir, encoding: 'utf-8', timeout: 5000 });
|
|
382
|
-
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
383
|
-
try {
|
|
384
|
-
// Handle special chars in commit messages
|
|
385
|
-
const sanitized = line.replace(/\\/g, '\\\\').replace(/(?<!\\)"/g, (match, offset) => {
|
|
386
|
-
// Only escape quotes inside the message field
|
|
387
|
-
return match;
|
|
388
|
-
});
|
|
389
|
-
return JSON.parse(sanitized);
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
// Fallback: parse manually
|
|
393
|
-
const hashMatch = line.match(/"hash":"([^"]+)"/);
|
|
394
|
-
const shortMatch = line.match(/"shortHash":"([^"]+)"/);
|
|
395
|
-
const authorMatch = line.match(/"author":"([^"]+)"/);
|
|
396
|
-
const dateMatch = line.match(/"date":"([^"]+)"/);
|
|
397
|
-
const messageMatch = line.match(/"message":"(.+?)","refs"/);
|
|
398
|
-
const refsMatch = line.match(/"refs":"([^"]*)"/);
|
|
399
|
-
return {
|
|
400
|
-
hash: hashMatch?.[1] || '',
|
|
401
|
-
shortHash: shortMatch?.[1] || '',
|
|
402
|
-
author: authorMatch?.[1] || '',
|
|
403
|
-
email: '',
|
|
404
|
-
date: dateMatch?.[1] || '',
|
|
405
|
-
message: messageMatch?.[1] || 'commit',
|
|
406
|
-
refs: refsMatch?.[1] || '',
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
// Also get graph lines
|
|
411
|
-
let graph = [];
|
|
412
|
-
try {
|
|
413
|
-
const graphOutput = execSync(`git log --graph --oneline --decorate -${count}`, { cwd: this.workingDir, encoding: 'utf-8', timeout: 5000 });
|
|
414
|
-
graph = graphOutput.trim().split('\n');
|
|
415
|
-
}
|
|
416
|
-
catch { }
|
|
417
|
-
sendJSON({ success: true, commits, graph });
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
sendJSON({ success: false, error: error.message, commits: [], graph: [] }, 500);
|
|
421
|
-
}
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
// File operations
|
|
425
|
-
if (pathname === '/api/files/list') {
|
|
426
|
-
const body = await parseBody();
|
|
427
|
-
const dirPath = body.path || '.';
|
|
428
|
-
const fullPath = path.resolve(this.workingDir, dirPath);
|
|
429
|
-
try {
|
|
430
|
-
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
431
|
-
const files = entries.map((entry) => ({
|
|
432
|
-
name: entry.name,
|
|
433
|
-
isDirectory: entry.isDirectory(),
|
|
434
|
-
path: path.join(dirPath, entry.name),
|
|
435
|
-
}));
|
|
436
|
-
sendJSON({ success: true, files });
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
// Recursive file tree for the WebUI file explorer
|
|
444
|
-
if (pathname === '/api/files/tree') {
|
|
445
|
-
const body = await parseBody();
|
|
446
|
-
const dirPath = body.path || '.';
|
|
447
|
-
const maxDepth = body.depth || 10;
|
|
448
|
-
const SKIP_DIRS = new Set([
|
|
449
|
-
'node_modules', '.git', 'dist', 'build', '.next', '.cache',
|
|
450
|
-
'__pycache__', '.venv', 'venv', '.tox', 'coverage', '.nyc_output',
|
|
451
|
-
'.svn', '.hg', 'bower_components', '.parcel-cache', '.turbo',
|
|
452
|
-
]);
|
|
453
|
-
const buildTree = async (currentPath, relativePath, depth) => {
|
|
454
|
-
if (depth <= 0)
|
|
455
|
-
return [];
|
|
456
|
-
const fullPath = path.resolve(this.workingDir, currentPath);
|
|
457
|
-
// Path traversal protection
|
|
458
|
-
if (!fullPath.startsWith(path.resolve(this.workingDir)))
|
|
459
|
-
return [];
|
|
460
|
-
try {
|
|
461
|
-
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
462
|
-
const nodes = [];
|
|
463
|
-
// Sort: directories first, then alphabetically
|
|
464
|
-
const sorted = entries.sort((a, b) => {
|
|
465
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
466
|
-
return -1;
|
|
467
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
468
|
-
return 1;
|
|
469
|
-
return a.name.localeCompare(b.name);
|
|
470
|
-
});
|
|
471
|
-
for (const entry of sorted) {
|
|
472
|
-
if (entry.name.startsWith('.') && entry.name !== '.env' && entry.name !== '.env.local') {
|
|
473
|
-
// Skip most hidden files/dirs but allow .env
|
|
474
|
-
if (entry.isDirectory())
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
if (entry.isDirectory() && SKIP_DIRS.has(entry.name))
|
|
478
|
-
continue;
|
|
479
|
-
const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
480
|
-
const node = {
|
|
481
|
-
name: entry.name,
|
|
482
|
-
path: entryRelPath,
|
|
483
|
-
isDirectory: entry.isDirectory(),
|
|
484
|
-
};
|
|
485
|
-
if (entry.isDirectory()) {
|
|
486
|
-
node.children = await buildTree(path.join(currentPath, entry.name), entryRelPath, depth - 1);
|
|
487
|
-
}
|
|
488
|
-
nodes.push(node);
|
|
489
|
-
}
|
|
490
|
-
return nodes;
|
|
491
|
-
}
|
|
492
|
-
catch {
|
|
493
|
-
return [];
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
try {
|
|
497
|
-
const tree = await buildTree(dirPath, '', maxDepth);
|
|
498
|
-
sendJSON({ success: true, tree });
|
|
499
|
-
}
|
|
500
|
-
catch (error) {
|
|
501
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
502
|
-
}
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (pathname === '/api/files/read') {
|
|
506
|
-
const body = await parseBody();
|
|
507
|
-
if (!body.path) {
|
|
508
|
-
sendJSON({ success: false, error: 'Missing path parameter' }, 400);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const fullPath = path.resolve(this.workingDir, body.path);
|
|
512
|
-
try {
|
|
513
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
514
|
-
sendJSON({ success: true, content, path: body.path });
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
518
|
-
}
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
// Serve raw binary files (images, videos, audio) for media preview
|
|
522
|
-
if (pathname === '/api/files/raw') {
|
|
523
|
-
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
524
|
-
const filePath = url.searchParams.get('path');
|
|
525
|
-
if (!filePath) {
|
|
526
|
-
sendJSON({ success: false, error: 'Missing path parameter' }, 400);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
const fullPath = path.resolve(this.workingDir, filePath);
|
|
530
|
-
// Path traversal protection
|
|
531
|
-
if (!fullPath.startsWith(path.resolve(this.workingDir))) {
|
|
532
|
-
sendJSON({ success: false, error: 'Invalid path' }, 403);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
const content = await fs.readFile(fullPath);
|
|
537
|
-
const ext = path.extname(fullPath).toLowerCase();
|
|
538
|
-
const mimeTypes = {
|
|
539
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
540
|
-
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.bmp': 'image/bmp',
|
|
541
|
-
'.avif': 'image/avif', '.tiff': 'image/tiff', '.tif': 'image/tiff',
|
|
542
|
-
'.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', '.mov': 'video/quicktime',
|
|
543
|
-
'.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
|
|
544
|
-
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac', '.aac': 'audio/aac',
|
|
545
|
-
'.m4a': 'audio/mp4', '.opus': 'audio/opus',
|
|
546
|
-
'.pdf': 'application/pdf',
|
|
547
|
-
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
|
|
548
|
-
};
|
|
549
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
550
|
-
res.writeHead(200, {
|
|
551
|
-
'Content-Type': contentType,
|
|
552
|
-
'Content-Length': content.length.toString(),
|
|
553
|
-
'Cache-Control': 'public, max-age=3600',
|
|
554
|
-
});
|
|
555
|
-
res.end(content);
|
|
556
|
-
}
|
|
557
|
-
catch (error) {
|
|
558
|
-
sendJSON({ success: false, error: error.message }, 404);
|
|
559
|
-
}
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
// Session management
|
|
563
|
-
if (pathname === '/api/session/create') {
|
|
564
|
-
const sessionId = this.createSession();
|
|
565
|
-
sendJSON({ success: true, sessionId });
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (pathname.startsWith('/api/session/') && pathname.endsWith('/message')) {
|
|
569
|
-
const sessionId = pathname.split('/')[3];
|
|
570
|
-
const body = await parseBody();
|
|
571
|
-
if (!body.message) {
|
|
572
|
-
sendJSON({ success: false, error: 'Missing message' }, 400);
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const session = this.sessions.get(sessionId);
|
|
576
|
-
if (!session) {
|
|
577
|
-
sendJSON({ success: false, error: 'Session not found' }, 404);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
// Start agent run - responses will be sent via WebSocket
|
|
581
|
-
this.runAgentMessage(sessionId, body.message);
|
|
582
|
-
sendJSON({ success: true, status: 'processing' });
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
// Test generation - use TestGenerator directly to avoid permission checks
|
|
586
|
-
if (pathname === '/api/tests/generate') {
|
|
587
|
-
const body = await parseBody();
|
|
588
|
-
if (!body.filePath) {
|
|
589
|
-
sendJSON({ success: false, error: 'Missing filePath parameter' }, 400);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
try {
|
|
593
|
-
const generator = new TestGenerator(this.workingDir);
|
|
594
|
-
// First analyze the file
|
|
595
|
-
const analysis = await generator.analyzeFile(body.filePath);
|
|
596
|
-
// Then generate tests from the analysis
|
|
597
|
-
const result = await generator.generateTests(analysis, {
|
|
598
|
-
framework: body.framework,
|
|
599
|
-
outputDir: body.outputDir,
|
|
600
|
-
includeEdgeCases: body.includeEdgeCases !== false,
|
|
601
|
-
includeMocks: body.includeMocks !== false,
|
|
602
|
-
maxTestsPerFunction: body.maxTestsPerFunction || 5,
|
|
603
|
-
});
|
|
604
|
-
// Write file if requested
|
|
605
|
-
if (body.writeFile && result.content) {
|
|
606
|
-
await writeTestFile(result);
|
|
607
|
-
sendJSON({ ...result, outputPath: result.testFilePath, success: true });
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
sendJSON({ ...result, success: true });
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
catch (error) {
|
|
614
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
615
|
-
}
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
if (pathname === '/api/tests/analyze') {
|
|
619
|
-
const body = await parseBody();
|
|
620
|
-
if (!body.filePath) {
|
|
621
|
-
sendJSON({ success: false, error: 'Missing filePath parameter' }, 400);
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
try {
|
|
625
|
-
const generator = new TestGenerator(this.workingDir);
|
|
626
|
-
const analysis = await generator.analyzeFile(body.filePath);
|
|
627
|
-
sendJSON({ success: true, analysis });
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
631
|
-
}
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
// Run tests
|
|
635
|
-
if (pathname === '/api/tests/run') {
|
|
636
|
-
const body = await parseBody();
|
|
637
|
-
const toolExecutor = new CodingToolExecutor(this.workingDir);
|
|
638
|
-
const result = await toolExecutor.execute('run_tests', {
|
|
639
|
-
command: body.command,
|
|
640
|
-
cwd: body.cwd,
|
|
641
|
-
});
|
|
642
|
-
sendJSON(result);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
// Environment variables (.env file management)
|
|
646
|
-
if (pathname === '/api/env') {
|
|
647
|
-
if (req.method === 'GET') {
|
|
648
|
-
// Auto-detect .env file in the working directory
|
|
649
|
-
const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
|
|
650
|
-
let envFilePath = null;
|
|
651
|
-
let envContent = '';
|
|
652
|
-
for (const envFile of envFiles) {
|
|
653
|
-
const fullEnvPath = path.join(this.workingDir, envFile);
|
|
654
|
-
try {
|
|
655
|
-
envContent = await fs.readFile(fullEnvPath, 'utf-8');
|
|
656
|
-
envFilePath = envFile;
|
|
657
|
-
break;
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
// File doesn't exist, try next
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (!envFilePath) {
|
|
664
|
-
// No .env file found, return empty state with suggested path
|
|
665
|
-
sendJSON({
|
|
666
|
-
success: true,
|
|
667
|
-
exists: false,
|
|
668
|
-
path: '.env',
|
|
669
|
-
fullPath: path.join(this.workingDir, '.env'),
|
|
670
|
-
variables: [],
|
|
671
|
-
raw: '',
|
|
672
|
-
});
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
// Parse the .env file content into structured variables
|
|
676
|
-
const variables = [];
|
|
677
|
-
const lines = envContent.split('\n');
|
|
678
|
-
for (const line of lines) {
|
|
679
|
-
const trimmed = line.trim();
|
|
680
|
-
if (trimmed === '') {
|
|
681
|
-
variables.push({ key: '', value: '', isComment: false, raw: line });
|
|
682
|
-
}
|
|
683
|
-
else if (trimmed.startsWith('#')) {
|
|
684
|
-
variables.push({ key: '', value: '', comment: trimmed.slice(1).trim(), isComment: true, raw: line });
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
const eqIndex = trimmed.indexOf('=');
|
|
688
|
-
if (eqIndex !== -1) {
|
|
689
|
-
const key = trimmed.substring(0, eqIndex).trim();
|
|
690
|
-
let value = trimmed.substring(eqIndex + 1).trim();
|
|
691
|
-
// Remove surrounding quotes if present
|
|
692
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
693
|
-
value = value.slice(1, -1);
|
|
694
|
-
}
|
|
695
|
-
variables.push({ key, value, isComment: false, raw: line });
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
variables.push({ key: trimmed, value: '', isComment: false, raw: line });
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
sendJSON({
|
|
703
|
-
success: true,
|
|
704
|
-
exists: true,
|
|
705
|
-
path: envFilePath,
|
|
706
|
-
fullPath: path.join(this.workingDir, envFilePath),
|
|
707
|
-
variables,
|
|
708
|
-
raw: envContent,
|
|
709
|
-
});
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
if (req.method === 'PUT') {
|
|
713
|
-
const body = await parseBody();
|
|
714
|
-
const envFileName = body.path || '.env';
|
|
715
|
-
const fullEnvPath = path.join(this.workingDir, envFileName);
|
|
716
|
-
// Path traversal protection
|
|
717
|
-
if (!fullEnvPath.startsWith(path.resolve(this.workingDir))) {
|
|
718
|
-
sendJSON({ success: false, error: 'Invalid path' }, 400);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
try {
|
|
722
|
-
if (body.raw !== undefined) {
|
|
723
|
-
// Write raw content directly
|
|
724
|
-
await fs.writeFile(fullEnvPath, body.raw, 'utf-8');
|
|
725
|
-
}
|
|
726
|
-
else if (body.variables) {
|
|
727
|
-
// Build .env content from structured variables
|
|
728
|
-
const lines = [];
|
|
729
|
-
for (const v of body.variables) {
|
|
730
|
-
if (v.isComment) {
|
|
731
|
-
lines.push(`# ${v.comment || ''}`);
|
|
732
|
-
}
|
|
733
|
-
else if (v.key === '' && v.value === '') {
|
|
734
|
-
lines.push('');
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
const needsQuotes = v.value && (v.value.includes(' ') || v.value.includes('#') || v.value.includes('"'));
|
|
738
|
-
const quotedValue = needsQuotes ? `"${v.value}"` : (v.value || '');
|
|
739
|
-
lines.push(`${v.key}=${quotedValue}`);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
await fs.writeFile(fullEnvPath, lines.join('\n') + '\n', 'utf-8');
|
|
743
|
-
}
|
|
744
|
-
else {
|
|
745
|
-
sendJSON({ success: false, error: 'Missing raw or variables field' }, 400);
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
sendJSON({ success: true, path: envFileName, fullPath: fullEnvPath });
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
752
|
-
}
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
// File list for @ command (simple GET)
|
|
757
|
-
if (pathname === '/api/files') {
|
|
758
|
-
try {
|
|
759
|
-
const files = [];
|
|
760
|
-
const walkDir = async (dir, prefix = '') => {
|
|
761
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
762
|
-
for (const entry of entries) {
|
|
763
|
-
// Skip hidden files, node_modules, dist, etc.
|
|
764
|
-
if (entry.name.startsWith('.') ||
|
|
765
|
-
entry.name === 'node_modules' ||
|
|
766
|
-
entry.name === 'dist' ||
|
|
767
|
-
entry.name === 'build' ||
|
|
768
|
-
entry.name === 'coverage' ||
|
|
769
|
-
entry.name === '__pycache__') {
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
773
|
-
if (entry.isDirectory()) {
|
|
774
|
-
files.push(relativePath + '/');
|
|
775
|
-
// Limit depth to avoid huge lists
|
|
776
|
-
if (relativePath.split('/').length < 4) {
|
|
777
|
-
await walkDir(path.join(dir, entry.name), relativePath);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
else {
|
|
781
|
-
files.push(relativePath);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
await walkDir(this.workingDir);
|
|
786
|
-
// Sort: directories first, then files
|
|
787
|
-
files.sort((a, b) => {
|
|
788
|
-
const aIsDir = a.endsWith('/');
|
|
789
|
-
const bIsDir = b.endsWith('/');
|
|
790
|
-
if (aIsDir && !bIsDir)
|
|
791
|
-
return -1;
|
|
792
|
-
if (!aIsDir && bIsDir)
|
|
793
|
-
return 1;
|
|
794
|
-
return a.localeCompare(b);
|
|
795
|
-
});
|
|
796
|
-
sendJSON({ success: true, files: files.slice(0, 500) }); // Limit to 500 files
|
|
797
|
-
}
|
|
798
|
-
catch (error) {
|
|
799
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
800
|
-
}
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
// Chat history API
|
|
804
|
-
if (pathname === '/api/history') {
|
|
805
|
-
if (req.method === 'GET') {
|
|
806
|
-
try {
|
|
807
|
-
const conversations = await this.historyManager.list();
|
|
808
|
-
sendJSON({ success: true, conversations });
|
|
809
|
-
}
|
|
810
|
-
catch (error) {
|
|
811
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
812
|
-
}
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
if (req.method === 'POST') {
|
|
816
|
-
const body = await parseBody();
|
|
817
|
-
try {
|
|
818
|
-
if (body.conversation) {
|
|
819
|
-
await this.historyManager.save(body.conversation);
|
|
820
|
-
sendJSON({ success: true, id: body.conversation.id });
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
sendJSON({ success: false, error: 'Missing conversation data' }, 400);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
catch (error) {
|
|
827
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
828
|
-
}
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
// Load specific conversation
|
|
833
|
-
if (pathname.startsWith('/api/history/') && req.method === 'GET') {
|
|
834
|
-
const id = pathname.split('/')[3];
|
|
835
|
-
if (id) {
|
|
836
|
-
try {
|
|
837
|
-
const conversation = await this.historyManager.load(id);
|
|
838
|
-
if (conversation) {
|
|
839
|
-
sendJSON({ success: true, conversation });
|
|
840
|
-
}
|
|
841
|
-
else {
|
|
842
|
-
sendJSON({ success: false, error: 'Conversation not found' }, 404);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
847
|
-
}
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
// Delete specific conversation
|
|
852
|
-
if (pathname.startsWith('/api/history/') && req.method === 'DELETE') {
|
|
853
|
-
const id = pathname.split('/')[3];
|
|
854
|
-
if (id) {
|
|
855
|
-
try {
|
|
856
|
-
const deleted = await this.historyManager.delete(id);
|
|
857
|
-
sendJSON({ success: deleted, error: deleted ? undefined : 'Not found' });
|
|
858
|
-
}
|
|
859
|
-
catch (error) {
|
|
860
|
-
sendJSON({ success: false, error: error.message }, 500);
|
|
861
|
-
}
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
// 404 for unknown API routes
|
|
866
|
-
sendJSON({ error: 'Not found' }, 404);
|
|
867
|
-
}
|
|
868
|
-
catch (error) {
|
|
869
|
-
sendJSON({ error: error.message }, 500);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* Serve static files
|
|
874
|
-
*/
|
|
875
|
-
async serveStatic(req, res, pathname) {
|
|
876
|
-
const staticDir = this.config.staticDir;
|
|
877
|
-
// Default to index.html for SPA
|
|
878
|
-
let filePath = pathname === '/' ? '/index.html' : pathname;
|
|
879
|
-
let fullPath = path.join(staticDir, filePath);
|
|
880
|
-
// Check if file exists, otherwise serve index.html (SPA fallback)
|
|
881
|
-
if (!fsSync.existsSync(fullPath)) {
|
|
882
|
-
fullPath = path.join(staticDir, 'index.html');
|
|
883
|
-
}
|
|
884
|
-
try {
|
|
885
|
-
const content = await fs.readFile(fullPath);
|
|
886
|
-
const ext = path.extname(fullPath).toLowerCase();
|
|
887
|
-
const mimeTypes = {
|
|
888
|
-
'.html': 'text/html',
|
|
889
|
-
'.js': 'application/javascript',
|
|
890
|
-
'.css': 'text/css',
|
|
891
|
-
'.json': 'application/json',
|
|
892
|
-
'.png': 'image/png',
|
|
893
|
-
'.jpg': 'image/jpeg',
|
|
894
|
-
'.svg': 'image/svg+xml',
|
|
895
|
-
'.ico': 'image/x-icon',
|
|
896
|
-
'.woff': 'font/woff',
|
|
897
|
-
'.woff2': 'font/woff2',
|
|
898
|
-
};
|
|
899
|
-
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' });
|
|
900
|
-
res.end(content);
|
|
901
|
-
}
|
|
902
|
-
catch {
|
|
903
|
-
// Serve inline fallback HTML if no frontend build exists
|
|
904
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
905
|
-
res.end(this.getFallbackHTML());
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Handle WebSocket connections
|
|
910
|
-
*/
|
|
911
|
-
handleWebSocket(ws, req) {
|
|
912
|
-
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
913
|
-
const sessionId = url.searchParams.get('session');
|
|
914
|
-
const mode = url.searchParams.get('mode'); // 'bridge' for TUI sync, null for standalone
|
|
915
|
-
// Register with SessionBridge for TUI-WebUI sync
|
|
916
|
-
if (mode === 'bridge') {
|
|
917
|
-
SessionBridge.registerWebSocket(ws);
|
|
918
|
-
ws.on('message', async (data) => {
|
|
919
|
-
try {
|
|
920
|
-
const message = JSON.parse(data.toString());
|
|
921
|
-
if (message.type === 'ping') {
|
|
922
|
-
ws.send(JSON.stringify({ type: 'pong' }));
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
// Forward user messages to TUI via SessionBridge
|
|
926
|
-
if (message.type === 'message' && message.content) {
|
|
927
|
-
SessionBridge.onWebUIUserMessage(message.content);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
catch (error) {
|
|
931
|
-
console.error('WebSocket message error:', error);
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
ws.on('close', () => {
|
|
935
|
-
SessionBridge.unregisterWebSocket(ws);
|
|
936
|
-
});
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
// Terminal mode - spawn a real shell with PTY via Python bridge
|
|
940
|
-
if (mode === 'terminal') {
|
|
941
|
-
let ptyProcess = null;
|
|
942
|
-
ws.on('message', (data) => {
|
|
943
|
-
try {
|
|
944
|
-
const message = JSON.parse(data.toString());
|
|
945
|
-
if (message.type === 'terminal:create') {
|
|
946
|
-
const cwd = message.cwd ? path.resolve(this.workingDir, message.cwd) : this.workingDir;
|
|
947
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
948
|
-
const cols = message.cols || 120;
|
|
949
|
-
const rows = message.rows || 30;
|
|
950
|
-
// Python PTY bridge script - creates a real pseudo-terminal
|
|
951
|
-
const ptyBridge = `
|
|
952
|
-
import pty, os, sys, select, signal, struct, fcntl, termios
|
|
953
|
-
|
|
954
|
-
def set_winsize(fd, rows, cols):
|
|
955
|
-
s = struct.pack('HHHH', rows, cols, 0, 0)
|
|
956
|
-
fcntl.ioctl(fd, termios.TIOCSWINSZ, s)
|
|
957
|
-
|
|
958
|
-
master, slave = pty.openpty()
|
|
959
|
-
set_winsize(master, ${rows}, ${cols})
|
|
960
|
-
|
|
961
|
-
pid = os.fork()
|
|
962
|
-
if pid == 0:
|
|
963
|
-
os.close(master)
|
|
964
|
-
os.setsid()
|
|
965
|
-
os.dup2(slave, 0)
|
|
966
|
-
os.dup2(slave, 1)
|
|
967
|
-
os.dup2(slave, 2)
|
|
968
|
-
os.close(slave)
|
|
969
|
-
os.environ['TERM'] = 'xterm-256color'
|
|
970
|
-
os.environ['COLORTERM'] = 'truecolor'
|
|
971
|
-
os.chdir('${cwd.replace(/'/g, "\\'")}')
|
|
972
|
-
os.execvp('${shell}', ['${shell}', '-i'])
|
|
973
|
-
else:
|
|
974
|
-
os.close(slave)
|
|
975
|
-
stdin_fd = sys.stdin.fileno()
|
|
976
|
-
stdout_fd = sys.stdout.fileno()
|
|
977
|
-
try:
|
|
978
|
-
while True:
|
|
979
|
-
r, _, _ = select.select([stdin_fd, master], [], [], 0.02)
|
|
980
|
-
if stdin_fd in r:
|
|
981
|
-
d = os.read(stdin_fd, 4096)
|
|
982
|
-
if not d: break
|
|
983
|
-
os.write(master, d)
|
|
984
|
-
if master in r:
|
|
985
|
-
try:
|
|
986
|
-
d = os.read(master, 4096)
|
|
987
|
-
except OSError: break
|
|
988
|
-
if not d: break
|
|
989
|
-
os.write(stdout_fd, d)
|
|
990
|
-
except (IOError, OSError): pass
|
|
991
|
-
finally:
|
|
992
|
-
try: os.kill(pid, signal.SIGTERM)
|
|
993
|
-
except: pass
|
|
994
|
-
`;
|
|
995
|
-
ptyProcess = spawn('python3', ['-u', '-c', ptyBridge], {
|
|
996
|
-
cwd,
|
|
997
|
-
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
998
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
999
|
-
});
|
|
1000
|
-
if (ptyProcess.stdout) {
|
|
1001
|
-
ptyProcess.stdout.on('data', (chunk) => {
|
|
1002
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1003
|
-
ws.send(JSON.stringify({ type: 'terminal:output', data: chunk.toString('utf-8') }));
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
if (ptyProcess.stderr) {
|
|
1008
|
-
ptyProcess.stderr.on('data', (chunk) => {
|
|
1009
|
-
// stderr from the PTY bridge (mostly shell startup messages)
|
|
1010
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1011
|
-
ws.send(JSON.stringify({ type: 'terminal:output', data: chunk.toString('utf-8') }));
|
|
1012
|
-
}
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
ptyProcess.on('exit', (code) => {
|
|
1016
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1017
|
-
ws.send(JSON.stringify({ type: 'terminal:output', data: `\r\n\x1b[90mShell exited (code ${code})\x1b[0m\r\n` }));
|
|
1018
|
-
}
|
|
1019
|
-
});
|
|
1020
|
-
ptyProcess.on('error', (err) => {
|
|
1021
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1022
|
-
ws.send(JSON.stringify({ type: 'terminal:output', data: `\r\n\x1b[31mError: ${err.message}\x1b[0m\r\n` }));
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1025
|
-
ws.send(JSON.stringify({ type: 'terminal:created', pid: ptyProcess.pid }));
|
|
1026
|
-
}
|
|
1027
|
-
if (message.type === 'terminal:input' && ptyProcess?.stdin) {
|
|
1028
|
-
ptyProcess.stdin.write(message.data);
|
|
1029
|
-
}
|
|
1030
|
-
if (message.type === 'terminal:resize' && ptyProcess?.pid) {
|
|
1031
|
-
// For resize, we'd need to signal the Python bridge
|
|
1032
|
-
// The Python bridge will handle SIGWINCH
|
|
1033
|
-
try {
|
|
1034
|
-
process.kill(ptyProcess.pid, 'SIGWINCH');
|
|
1035
|
-
}
|
|
1036
|
-
catch {
|
|
1037
|
-
// ignore
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
catch (error) {
|
|
1042
|
-
console.error('Terminal WebSocket error:', error);
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
1045
|
-
ws.on('close', () => {
|
|
1046
|
-
if (ptyProcess) {
|
|
1047
|
-
try {
|
|
1048
|
-
ptyProcess.kill('SIGTERM');
|
|
1049
|
-
setTimeout(() => {
|
|
1050
|
-
try {
|
|
1051
|
-
ptyProcess?.kill('SIGKILL');
|
|
1052
|
-
}
|
|
1053
|
-
catch { }
|
|
1054
|
-
}, 1000);
|
|
1055
|
-
}
|
|
1056
|
-
catch { }
|
|
1057
|
-
ptyProcess = null;
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
// Legacy standalone mode for backward compatibility
|
|
1063
|
-
if (sessionId) {
|
|
1064
|
-
this.wsClients.set(sessionId, ws);
|
|
1065
|
-
}
|
|
1066
|
-
ws.on('message', async (data) => {
|
|
1067
|
-
try {
|
|
1068
|
-
const message = JSON.parse(data.toString());
|
|
1069
|
-
if (message.type === 'ping') {
|
|
1070
|
-
ws.send(JSON.stringify({ type: 'pong' }));
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
if (message.type === 'message' && message.sessionId && message.content) {
|
|
1074
|
-
this.runAgentMessage(message.sessionId, message.content);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
catch (error) {
|
|
1078
|
-
console.error('WebSocket message error:', error);
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
|
-
ws.on('close', () => {
|
|
1082
|
-
if (sessionId) {
|
|
1083
|
-
this.wsClients.delete(sessionId);
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* Create a new agent session
|
|
1089
|
-
*/
|
|
1090
|
-
createSession() {
|
|
1091
|
-
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1092
|
-
const toolExecutor = new CodingToolExecutor(this.workingDir);
|
|
1093
|
-
this.sessions.set(sessionId, {
|
|
1094
|
-
id: sessionId,
|
|
1095
|
-
agent: null,
|
|
1096
|
-
toolExecutor,
|
|
1097
|
-
messages: [],
|
|
1098
|
-
status: 'idle',
|
|
1099
|
-
createdAt: new Date(),
|
|
1100
|
-
lastActivity: new Date(),
|
|
1101
|
-
});
|
|
1102
|
-
return sessionId;
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Run agent with a message
|
|
1106
|
-
*/
|
|
1107
|
-
async runAgentMessage(sessionId, message) {
|
|
1108
|
-
const session = this.sessions.get(sessionId);
|
|
1109
|
-
if (!session)
|
|
1110
|
-
return;
|
|
1111
|
-
const ws = this.wsClients.get(sessionId);
|
|
1112
|
-
const send = (data) => {
|
|
1113
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1114
|
-
ws.send(JSON.stringify(data));
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
try {
|
|
1118
|
-
session.status = 'running';
|
|
1119
|
-
session.lastActivity = new Date();
|
|
1120
|
-
const apiKey = this.configManager.getApiKey();
|
|
1121
|
-
if (!apiKey) {
|
|
1122
|
-
send({ type: 'error', error: 'API key not configured' });
|
|
1123
|
-
session.status = 'error';
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
// Create agent if not exists
|
|
1127
|
-
if (!session.agent) {
|
|
1128
|
-
if (!session.builtInSkillsPrompt) {
|
|
1129
|
-
const sm = new SkillManager(this.workingDir, apiKey, this.configManager.getBaseUrl(), this.configManager.getModel(), this.configManager.get('provider'));
|
|
1130
|
-
await sm.loadSkills();
|
|
1131
|
-
const autoSkillsShEnabled = process.env.XIBECODE_AUTO_SKILLS_SH === '1' || process.env.XIBECODE_AUTO_SKILLS_SH === 'true';
|
|
1132
|
-
let autoInstalledSkillNames = [];
|
|
1133
|
-
if (autoSkillsShEnabled) {
|
|
1134
|
-
const auto = await sm.autoInstallFromSkillsShForTask(message, { enabled: true, maxInstalls: 1 });
|
|
1135
|
-
autoInstalledSkillNames = auto.installedSkillNames || [];
|
|
1136
|
-
}
|
|
1137
|
-
session.builtInSkillsPrompt = await sm.buildDefaultSkillsPromptForTask(message, this.workingDir);
|
|
1138
|
-
for (const name of autoInstalledSkillNames) {
|
|
1139
|
-
const s = sm.getSkill(name);
|
|
1140
|
-
if (!s?.instructions)
|
|
1141
|
-
continue;
|
|
1142
|
-
session.builtInSkillsPrompt += `\n\n---\n\n## Auto-installed skills.sh skill\n\n### ${s.name}\n*${s.description}*\n\n${s.instructions}`;
|
|
1143
|
-
break;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
session.agent = new EnhancedAgent({
|
|
1147
|
-
apiKey,
|
|
1148
|
-
baseUrl: this.configManager.getBaseUrl(),
|
|
1149
|
-
model: this.configManager.getModel(),
|
|
1150
|
-
maxIterations: this.configManager.get('maxIterations') || 50,
|
|
1151
|
-
provider: this.configManager.get('provider'),
|
|
1152
|
-
customProviderFormat: this.configManager.get('customProviderFormat'),
|
|
1153
|
-
requestFormat: this.configManager.get('requestFormat') ?? 'auto',
|
|
1154
|
-
defaultSkillsPrompt: session.builtInSkillsPrompt,
|
|
1155
|
-
}, this.configManager.get('provider'));
|
|
1156
|
-
}
|
|
1157
|
-
const agent = session.agent;
|
|
1158
|
-
// Subscribe to agent events
|
|
1159
|
-
agent.on('event', (event) => {
|
|
1160
|
-
switch (event.type) {
|
|
1161
|
-
case 'thinking':
|
|
1162
|
-
send({ type: 'thinking', message: event.data.message });
|
|
1163
|
-
break;
|
|
1164
|
-
case 'stream_start':
|
|
1165
|
-
send({ type: 'stream_start', persona: event.data.persona });
|
|
1166
|
-
break;
|
|
1167
|
-
case 'stream_text':
|
|
1168
|
-
send({ type: 'stream_text', text: event.data.text });
|
|
1169
|
-
break;
|
|
1170
|
-
case 'stream_end':
|
|
1171
|
-
send({ type: 'stream_end' });
|
|
1172
|
-
break;
|
|
1173
|
-
case 'tool_call':
|
|
1174
|
-
send({
|
|
1175
|
-
type: 'tool_call',
|
|
1176
|
-
name: event.data.name,
|
|
1177
|
-
input: event.data.input,
|
|
1178
|
-
});
|
|
1179
|
-
break;
|
|
1180
|
-
case 'tool_result':
|
|
1181
|
-
send({
|
|
1182
|
-
type: 'tool_result',
|
|
1183
|
-
name: event.data.name,
|
|
1184
|
-
success: event.data.success,
|
|
1185
|
-
});
|
|
1186
|
-
break;
|
|
1187
|
-
case 'response':
|
|
1188
|
-
send({ type: 'response', text: event.data.text });
|
|
1189
|
-
break;
|
|
1190
|
-
case 'complete':
|
|
1191
|
-
send({ type: 'complete', stats: event.data });
|
|
1192
|
-
session.status = 'idle';
|
|
1193
|
-
break;
|
|
1194
|
-
case 'error':
|
|
1195
|
-
send({ type: 'error', error: event.data.message || event.data.error });
|
|
1196
|
-
break;
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
// Run agent
|
|
1200
|
-
const tools = session.toolExecutor.getTools();
|
|
1201
|
-
const refs = extractAtReferences(message, this.workingDir);
|
|
1202
|
-
const { image: imageRefs } = splitAtReferences(refs);
|
|
1203
|
-
const images = [];
|
|
1204
|
-
for (const ref of imageRefs) {
|
|
1205
|
-
try {
|
|
1206
|
-
const mime = mimeFromExtension(ref.extension);
|
|
1207
|
-
if (!mime)
|
|
1208
|
-
continue;
|
|
1209
|
-
const attachment = await loadImageAttachment(ref.resolvedPath, { mime });
|
|
1210
|
-
images.push(attachment);
|
|
1211
|
-
}
|
|
1212
|
-
catch {
|
|
1213
|
-
// Ignore image load failures so text chat still works.
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
await agent.run(message, tools, session.toolExecutor, images.length ? { images } : undefined);
|
|
1217
|
-
session.messages.push({ role: 'user', content: message });
|
|
1218
|
-
session.status = 'idle';
|
|
1219
|
-
}
|
|
1220
|
-
catch (error) {
|
|
1221
|
-
send({ type: 'error', error: error.message });
|
|
1222
|
-
session.status = 'error';
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
/**
|
|
1226
|
-
* Get project information
|
|
1227
|
-
*/
|
|
1228
|
-
async getProjectInfo() {
|
|
1229
|
-
const gitUtils = new GitUtils(this.workingDir);
|
|
1230
|
-
const testRunner = new TestRunnerDetector(this.workingDir);
|
|
1231
|
-
let gitInfo = null;
|
|
1232
|
-
try {
|
|
1233
|
-
gitInfo = await gitUtils.getStatus();
|
|
1234
|
-
}
|
|
1235
|
-
catch {
|
|
1236
|
-
// Not a git repo
|
|
1237
|
-
}
|
|
1238
|
-
let testInfo = null;
|
|
1239
|
-
try {
|
|
1240
|
-
testInfo = await testRunner.detectTestRunner();
|
|
1241
|
-
}
|
|
1242
|
-
catch {
|
|
1243
|
-
// No test runner
|
|
1244
|
-
}
|
|
1245
|
-
// Read package.json if exists
|
|
1246
|
-
let packageJson = null;
|
|
1247
|
-
try {
|
|
1248
|
-
const pkgPath = path.join(this.workingDir, 'package.json');
|
|
1249
|
-
packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
1250
|
-
}
|
|
1251
|
-
catch {
|
|
1252
|
-
// No package.json
|
|
1253
|
-
}
|
|
1254
|
-
return {
|
|
1255
|
-
name: packageJson?.name || path.basename(this.workingDir),
|
|
1256
|
-
version: packageJson?.version,
|
|
1257
|
-
description: packageJson?.description,
|
|
1258
|
-
workingDir: this.workingDir,
|
|
1259
|
-
isGitRepo: !!gitInfo,
|
|
1260
|
-
gitBranch: gitInfo?.branch,
|
|
1261
|
-
gitStatus: gitInfo?.clean ? 'clean' : 'dirty',
|
|
1262
|
-
testRunner: testInfo?.runner,
|
|
1263
|
-
testCommand: testInfo?.command,
|
|
1264
|
-
packageManager: testInfo?.packageManager,
|
|
1265
|
-
dependencies: packageJson?.dependencies ? Object.keys(packageJson.dependencies).length : 0,
|
|
1266
|
-
devDependencies: packageJson?.devDependencies ? Object.keys(packageJson.devDependencies).length : 0,
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
/**
|
|
1270
|
-
* Fallback HTML when no frontend build exists
|
|
1271
|
-
* Minimalistic terminal-style WebUI
|
|
1272
|
-
*/
|
|
1273
|
-
getFallbackHTML() {
|
|
1274
|
-
return `<!DOCTYPE html>
|
|
1275
|
-
<html lang="en">
|
|
1276
|
-
<head>
|
|
1277
|
-
<meta charset="UTF-8">
|
|
1278
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1279
|
-
<title>XibeCode</title>
|
|
1280
|
-
<style>
|
|
1281
|
-
:root {
|
|
1282
|
-
--bg-primary: #0d1117;
|
|
1283
|
-
--bg-secondary: #161b22;
|
|
1284
|
-
--bg-tertiary: #21262d;
|
|
1285
|
-
--border-color: #30363d;
|
|
1286
|
-
--text-primary: #e6edf3;
|
|
1287
|
-
--text-secondary: #8b949e;
|
|
1288
|
-
--text-muted: #6e7681;
|
|
1289
|
-
--accent-blue: #58a6ff;
|
|
1290
|
-
--accent-green: #3fb950;
|
|
1291
|
-
--accent-yellow: #d29922;
|
|
1292
|
-
--accent-red: #f85149;
|
|
1293
|
-
--accent-purple: #a371f7;
|
|
1294
|
-
--accent-cyan: #39c5cf;
|
|
1295
|
-
}
|
|
1296
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1297
|
-
body {
|
|
1298
|
-
font-family: 'SF Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
|
|
1299
|
-
background: var(--bg-primary);
|
|
1300
|
-
color: var(--text-primary);
|
|
1301
|
-
min-height: 100vh;
|
|
1302
|
-
display: flex;
|
|
1303
|
-
flex-direction: column;
|
|
1304
|
-
font-size: 14px;
|
|
1305
|
-
line-height: 1.5;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
/* Header */
|
|
1309
|
-
.header {
|
|
1310
|
-
background: var(--bg-secondary);
|
|
1311
|
-
border-bottom: 1px solid var(--border-color);
|
|
1312
|
-
padding: 12px 20px;
|
|
1313
|
-
display: flex;
|
|
1314
|
-
align-items: center;
|
|
1315
|
-
justify-content: space-between;
|
|
1316
|
-
flex-wrap: wrap;
|
|
1317
|
-
gap: 12px;
|
|
1318
|
-
}
|
|
1319
|
-
.logo {
|
|
1320
|
-
display: flex;
|
|
1321
|
-
align-items: center;
|
|
1322
|
-
gap: 10px;
|
|
1323
|
-
}
|
|
1324
|
-
.logo-icon {
|
|
1325
|
-
width: 28px;
|
|
1326
|
-
height: 28px;
|
|
1327
|
-
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
|
|
1328
|
-
border-radius: 6px;
|
|
1329
|
-
display: flex;
|
|
1330
|
-
align-items: center;
|
|
1331
|
-
justify-content: center;
|
|
1332
|
-
font-weight: bold;
|
|
1333
|
-
font-size: 16px;
|
|
1334
|
-
}
|
|
1335
|
-
.logo-text {
|
|
1336
|
-
font-size: 18px;
|
|
1337
|
-
font-weight: 600;
|
|
1338
|
-
color: var(--text-primary);
|
|
1339
|
-
}
|
|
1340
|
-
.logo-text span { color: var(--accent-cyan); }
|
|
1341
|
-
.header-info {
|
|
1342
|
-
display: flex;
|
|
1343
|
-
align-items: center;
|
|
1344
|
-
gap: 16px;
|
|
1345
|
-
flex-wrap: wrap;
|
|
1346
|
-
}
|
|
1347
|
-
.info-item {
|
|
1348
|
-
display: flex;
|
|
1349
|
-
align-items: center;
|
|
1350
|
-
gap: 6px;
|
|
1351
|
-
font-size: 12px;
|
|
1352
|
-
color: var(--text-secondary);
|
|
1353
|
-
}
|
|
1354
|
-
.info-item .label { color: var(--text-muted); }
|
|
1355
|
-
.info-item .value { color: var(--accent-cyan); }
|
|
1356
|
-
.status-dot {
|
|
1357
|
-
width: 8px;
|
|
1358
|
-
height: 8px;
|
|
1359
|
-
border-radius: 50%;
|
|
1360
|
-
background: var(--accent-red);
|
|
1361
|
-
}
|
|
1362
|
-
.status-dot.connected { background: var(--accent-green); }
|
|
1363
|
-
|
|
1364
|
-
/* Main layout */
|
|
1365
|
-
.main {
|
|
1366
|
-
flex: 1;
|
|
1367
|
-
display: flex;
|
|
1368
|
-
flex-direction: column;
|
|
1369
|
-
max-width: 1200px;
|
|
1370
|
-
width: 100%;
|
|
1371
|
-
margin: 0 auto;
|
|
1372
|
-
padding: 16px;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
/* Messages area */
|
|
1376
|
-
.messages {
|
|
1377
|
-
flex: 1;
|
|
1378
|
-
overflow-y: auto;
|
|
1379
|
-
padding: 16px 0;
|
|
1380
|
-
display: flex;
|
|
1381
|
-
flex-direction: column;
|
|
1382
|
-
gap: 16px;
|
|
1383
|
-
min-height: 400px;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/* Message styles */
|
|
1387
|
-
.message {
|
|
1388
|
-
padding: 12px 16px;
|
|
1389
|
-
border-radius: 8px;
|
|
1390
|
-
max-width: 85%;
|
|
1391
|
-
word-wrap: break-word;
|
|
1392
|
-
}
|
|
1393
|
-
.message.user {
|
|
1394
|
-
background: var(--bg-tertiary);
|
|
1395
|
-
border: 1px solid var(--border-color);
|
|
1396
|
-
align-self: flex-end;
|
|
1397
|
-
margin-left: auto;
|
|
1398
|
-
}
|
|
1399
|
-
.message.user::before {
|
|
1400
|
-
content: '> ';
|
|
1401
|
-
color: var(--accent-green);
|
|
1402
|
-
}
|
|
1403
|
-
.message.assistant {
|
|
1404
|
-
background: var(--bg-secondary);
|
|
1405
|
-
border-left: 3px solid var(--accent-cyan);
|
|
1406
|
-
align-self: flex-start;
|
|
1407
|
-
}
|
|
1408
|
-
.message.system {
|
|
1409
|
-
background: transparent;
|
|
1410
|
-
color: var(--text-muted);
|
|
1411
|
-
font-size: 12px;
|
|
1412
|
-
text-align: center;
|
|
1413
|
-
align-self: center;
|
|
1414
|
-
max-width: 100%;
|
|
1415
|
-
}
|
|
1416
|
-
.message.tool {
|
|
1417
|
-
background: var(--bg-tertiary);
|
|
1418
|
-
border-left: 3px solid var(--accent-yellow);
|
|
1419
|
-
font-size: 13px;
|
|
1420
|
-
padding: 10px 14px;
|
|
1421
|
-
}
|
|
1422
|
-
.message.tool .tool-name {
|
|
1423
|
-
color: var(--accent-yellow);
|
|
1424
|
-
font-weight: 600;
|
|
1425
|
-
}
|
|
1426
|
-
.message.tool .tool-status {
|
|
1427
|
-
color: var(--text-muted);
|
|
1428
|
-
margin-left: 8px;
|
|
1429
|
-
}
|
|
1430
|
-
.message.tool .tool-status.success { color: var(--accent-green); }
|
|
1431
|
-
.message.tool .tool-status.error { color: var(--accent-red); }
|
|
1432
|
-
|
|
1433
|
-
/* Thinking indicator */
|
|
1434
|
-
.thinking {
|
|
1435
|
-
display: none;
|
|
1436
|
-
align-items: center;
|
|
1437
|
-
gap: 10px;
|
|
1438
|
-
padding: 12px 16px;
|
|
1439
|
-
color: var(--text-secondary);
|
|
1440
|
-
font-size: 13px;
|
|
1441
|
-
}
|
|
1442
|
-
.thinking.visible { display: flex; }
|
|
1443
|
-
.spinner {
|
|
1444
|
-
width: 16px;
|
|
1445
|
-
height: 16px;
|
|
1446
|
-
border: 2px solid var(--border-color);
|
|
1447
|
-
border-top-color: var(--accent-cyan);
|
|
1448
|
-
border-radius: 50%;
|
|
1449
|
-
animation: spin 0.8s linear infinite;
|
|
1450
|
-
}
|
|
1451
|
-
@keyframes spin {
|
|
1452
|
-
to { transform: rotate(360deg); }
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
/* Input area */
|
|
1456
|
-
.input-area {
|
|
1457
|
-
background: var(--bg-secondary);
|
|
1458
|
-
border: 1px solid var(--border-color);
|
|
1459
|
-
border-radius: 8px;
|
|
1460
|
-
padding: 12px;
|
|
1461
|
-
margin-top: 16px;
|
|
1462
|
-
position: relative;
|
|
1463
|
-
}
|
|
1464
|
-
.input-wrapper {
|
|
1465
|
-
display: flex;
|
|
1466
|
-
gap: 12px;
|
|
1467
|
-
align-items: flex-end;
|
|
1468
|
-
}
|
|
1469
|
-
.input-field {
|
|
1470
|
-
flex: 1;
|
|
1471
|
-
background: transparent;
|
|
1472
|
-
border: none;
|
|
1473
|
-
color: var(--text-primary);
|
|
1474
|
-
font-family: inherit;
|
|
1475
|
-
font-size: 14px;
|
|
1476
|
-
resize: none;
|
|
1477
|
-
min-height: 24px;
|
|
1478
|
-
max-height: 200px;
|
|
1479
|
-
outline: none;
|
|
1480
|
-
}
|
|
1481
|
-
.input-field::placeholder { color: var(--text-muted); }
|
|
1482
|
-
.send-btn {
|
|
1483
|
-
background: var(--accent-cyan);
|
|
1484
|
-
color: var(--bg-primary);
|
|
1485
|
-
border: none;
|
|
1486
|
-
padding: 8px 16px;
|
|
1487
|
-
border-radius: 6px;
|
|
1488
|
-
font-family: inherit;
|
|
1489
|
-
font-weight: 600;
|
|
1490
|
-
cursor: pointer;
|
|
1491
|
-
transition: opacity 0.2s;
|
|
1492
|
-
}
|
|
1493
|
-
.send-btn:hover { opacity: 0.9; }
|
|
1494
|
-
.send-btn:disabled {
|
|
1495
|
-
opacity: 0.4;
|
|
1496
|
-
cursor: not-allowed;
|
|
1497
|
-
}
|
|
1498
|
-
.input-hints {
|
|
1499
|
-
display: flex;
|
|
1500
|
-
gap: 16px;
|
|
1501
|
-
margin-top: 8px;
|
|
1502
|
-
font-size: 12px;
|
|
1503
|
-
color: var(--text-muted);
|
|
1504
|
-
}
|
|
1505
|
-
.hint-key {
|
|
1506
|
-
background: var(--bg-tertiary);
|
|
1507
|
-
padding: 2px 6px;
|
|
1508
|
-
border-radius: 3px;
|
|
1509
|
-
font-size: 11px;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
/* Command popup */
|
|
1513
|
-
.cmd-popup {
|
|
1514
|
-
display: none;
|
|
1515
|
-
position: absolute;
|
|
1516
|
-
bottom: 100%;
|
|
1517
|
-
left: 0;
|
|
1518
|
-
right: 0;
|
|
1519
|
-
background: var(--bg-secondary);
|
|
1520
|
-
border: 1px solid var(--border-color);
|
|
1521
|
-
border-radius: 8px;
|
|
1522
|
-
margin-bottom: 8px;
|
|
1523
|
-
max-height: 300px;
|
|
1524
|
-
overflow-y: auto;
|
|
1525
|
-
z-index: 100;
|
|
1526
|
-
}
|
|
1527
|
-
.cmd-popup.visible { display: block; }
|
|
1528
|
-
.cmd-popup-header {
|
|
1529
|
-
padding: 10px 14px;
|
|
1530
|
-
border-bottom: 1px solid var(--border-color);
|
|
1531
|
-
font-size: 12px;
|
|
1532
|
-
color: var(--text-muted);
|
|
1533
|
-
font-weight: 600;
|
|
1534
|
-
}
|
|
1535
|
-
.cmd-item {
|
|
1536
|
-
padding: 10px 14px;
|
|
1537
|
-
cursor: pointer;
|
|
1538
|
-
display: flex;
|
|
1539
|
-
align-items: center;
|
|
1540
|
-
gap: 12px;
|
|
1541
|
-
border-bottom: 1px solid var(--border-color);
|
|
1542
|
-
}
|
|
1543
|
-
.cmd-item:last-child { border-bottom: none; }
|
|
1544
|
-
.cmd-item:hover { background: var(--bg-tertiary); }
|
|
1545
|
-
.cmd-item.selected { background: var(--bg-tertiary); }
|
|
1546
|
-
.cmd-item-icon {
|
|
1547
|
-
width: 24px;
|
|
1548
|
-
text-align: center;
|
|
1549
|
-
}
|
|
1550
|
-
.cmd-item-info { flex: 1; }
|
|
1551
|
-
.cmd-item-name {
|
|
1552
|
-
font-weight: 600;
|
|
1553
|
-
color: var(--text-primary);
|
|
1554
|
-
}
|
|
1555
|
-
.cmd-item-desc {
|
|
1556
|
-
font-size: 12px;
|
|
1557
|
-
color: var(--text-muted);
|
|
1558
|
-
}
|
|
1559
|
-
.cmd-item-color {
|
|
1560
|
-
width: 12px;
|
|
1561
|
-
height: 12px;
|
|
1562
|
-
border-radius: 3px;
|
|
1563
|
-
}
|
|
1564
|
-
.cmd-section-header {
|
|
1565
|
-
padding: 8px 14px;
|
|
1566
|
-
font-size: 11px;
|
|
1567
|
-
text-transform: uppercase;
|
|
1568
|
-
letter-spacing: 0.5px;
|
|
1569
|
-
color: var(--text-muted);
|
|
1570
|
-
background: var(--bg-tertiary);
|
|
1571
|
-
font-weight: 600;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
/* Settings panel */
|
|
1575
|
-
.settings-overlay {
|
|
1576
|
-
display: none;
|
|
1577
|
-
position: fixed;
|
|
1578
|
-
top: 0;
|
|
1579
|
-
left: 0;
|
|
1580
|
-
right: 0;
|
|
1581
|
-
bottom: 0;
|
|
1582
|
-
background: rgba(0,0,0,0.7);
|
|
1583
|
-
z-index: 200;
|
|
1584
|
-
align-items: center;
|
|
1585
|
-
justify-content: center;
|
|
1586
|
-
}
|
|
1587
|
-
.settings-overlay.visible {
|
|
1588
|
-
display: flex;
|
|
1589
|
-
}
|
|
1590
|
-
.settings-panel {
|
|
1591
|
-
background: var(--bg-secondary);
|
|
1592
|
-
border: 1px solid var(--border-color);
|
|
1593
|
-
border-radius: 12px;
|
|
1594
|
-
width: 90%;
|
|
1595
|
-
max-width: 500px;
|
|
1596
|
-
max-height: 80vh;
|
|
1597
|
-
overflow-y: auto;
|
|
1598
|
-
}
|
|
1599
|
-
.settings-header {
|
|
1600
|
-
padding: 16px 20px;
|
|
1601
|
-
border-bottom: 1px solid var(--border-color);
|
|
1602
|
-
display: flex;
|
|
1603
|
-
justify-content: space-between;
|
|
1604
|
-
align-items: center;
|
|
1605
|
-
}
|
|
1606
|
-
.settings-header h2 {
|
|
1607
|
-
font-size: 16px;
|
|
1608
|
-
font-weight: 600;
|
|
1609
|
-
}
|
|
1610
|
-
.settings-close {
|
|
1611
|
-
background: transparent;
|
|
1612
|
-
border: none;
|
|
1613
|
-
color: var(--text-secondary);
|
|
1614
|
-
cursor: pointer;
|
|
1615
|
-
font-size: 18px;
|
|
1616
|
-
padding: 4px;
|
|
1617
|
-
}
|
|
1618
|
-
.settings-close:hover { color: var(--text-primary); }
|
|
1619
|
-
.settings-content {
|
|
1620
|
-
padding: 20px;
|
|
1621
|
-
}
|
|
1622
|
-
.settings-section {
|
|
1623
|
-
margin-bottom: 24px;
|
|
1624
|
-
}
|
|
1625
|
-
.settings-section:last-child { margin-bottom: 0; }
|
|
1626
|
-
.settings-section h3 {
|
|
1627
|
-
font-size: 12px;
|
|
1628
|
-
text-transform: uppercase;
|
|
1629
|
-
color: var(--text-muted);
|
|
1630
|
-
margin-bottom: 12px;
|
|
1631
|
-
letter-spacing: 0.5px;
|
|
1632
|
-
}
|
|
1633
|
-
.settings-field {
|
|
1634
|
-
margin-bottom: 16px;
|
|
1635
|
-
}
|
|
1636
|
-
.settings-field:last-child { margin-bottom: 0; }
|
|
1637
|
-
.settings-field label {
|
|
1638
|
-
display: block;
|
|
1639
|
-
font-size: 13px;
|
|
1640
|
-
color: var(--text-secondary);
|
|
1641
|
-
margin-bottom: 6px;
|
|
1642
|
-
}
|
|
1643
|
-
.settings-field input,
|
|
1644
|
-
.settings-field select {
|
|
1645
|
-
width: 100%;
|
|
1646
|
-
background: var(--bg-tertiary);
|
|
1647
|
-
border: 1px solid var(--border-color);
|
|
1648
|
-
border-radius: 6px;
|
|
1649
|
-
padding: 10px 12px;
|
|
1650
|
-
color: var(--text-primary);
|
|
1651
|
-
font-family: inherit;
|
|
1652
|
-
font-size: 13px;
|
|
1653
|
-
}
|
|
1654
|
-
.settings-field input:focus,
|
|
1655
|
-
.settings-field select:focus {
|
|
1656
|
-
outline: none;
|
|
1657
|
-
border-color: var(--accent-cyan);
|
|
1658
|
-
}
|
|
1659
|
-
.settings-field input::placeholder { color: var(--text-muted); }
|
|
1660
|
-
.settings-field small {
|
|
1661
|
-
display: block;
|
|
1662
|
-
margin-top: 4px;
|
|
1663
|
-
font-size: 11px;
|
|
1664
|
-
color: var(--text-muted);
|
|
1665
|
-
}
|
|
1666
|
-
.settings-btn {
|
|
1667
|
-
width: 100%;
|
|
1668
|
-
background: var(--accent-cyan);
|
|
1669
|
-
color: var(--bg-primary);
|
|
1670
|
-
border: none;
|
|
1671
|
-
padding: 12px;
|
|
1672
|
-
border-radius: 6px;
|
|
1673
|
-
font-family: inherit;
|
|
1674
|
-
font-weight: 600;
|
|
1675
|
-
cursor: pointer;
|
|
1676
|
-
margin-top: 16px;
|
|
1677
|
-
}
|
|
1678
|
-
.settings-btn:hover { opacity: 0.9; }
|
|
1679
|
-
|
|
1680
|
-
/* Markdown rendering */
|
|
1681
|
-
.message.assistant code {
|
|
1682
|
-
background: var(--bg-tertiary);
|
|
1683
|
-
padding: 2px 6px;
|
|
1684
|
-
border-radius: 4px;
|
|
1685
|
-
font-size: 13px;
|
|
1686
|
-
}
|
|
1687
|
-
.message.assistant pre {
|
|
1688
|
-
background: var(--bg-primary);
|
|
1689
|
-
border: 1px solid var(--border-color);
|
|
1690
|
-
border-radius: 6px;
|
|
1691
|
-
padding: 12px;
|
|
1692
|
-
margin: 8px 0;
|
|
1693
|
-
overflow-x: auto;
|
|
1694
|
-
}
|
|
1695
|
-
.message.assistant pre code {
|
|
1696
|
-
background: none;
|
|
1697
|
-
padding: 0;
|
|
1698
|
-
}
|
|
1699
|
-
.message.assistant strong { color: var(--accent-cyan); }
|
|
1700
|
-
.message.assistant em { color: var(--text-secondary); }
|
|
1701
|
-
.message.assistant a {
|
|
1702
|
-
color: var(--accent-blue);
|
|
1703
|
-
text-decoration: none;
|
|
1704
|
-
}
|
|
1705
|
-
.message.assistant a:hover { text-decoration: underline; }
|
|
1706
|
-
.message.assistant ul, .message.assistant ol {
|
|
1707
|
-
margin: 8px 0;
|
|
1708
|
-
padding-left: 20px;
|
|
1709
|
-
}
|
|
1710
|
-
.message.assistant li { margin: 4px 0; }
|
|
1711
|
-
.message.assistant blockquote {
|
|
1712
|
-
border-left: 3px solid var(--border-color);
|
|
1713
|
-
padding-left: 12px;
|
|
1714
|
-
color: var(--text-secondary);
|
|
1715
|
-
margin: 8px 0;
|
|
1716
|
-
}
|
|
1717
|
-
.message.assistant h1, .message.assistant h2, .message.assistant h3 {
|
|
1718
|
-
margin: 16px 0 8px;
|
|
1719
|
-
color: var(--text-primary);
|
|
1720
|
-
}
|
|
1721
|
-
.message.assistant h1 { font-size: 18px; }
|
|
1722
|
-
.message.assistant h2 { font-size: 16px; }
|
|
1723
|
-
.message.assistant h3 { font-size: 14px; }
|
|
1724
|
-
|
|
1725
|
-
/* Settings button in header */
|
|
1726
|
-
.settings-trigger {
|
|
1727
|
-
background: transparent;
|
|
1728
|
-
border: 1px solid var(--border-color);
|
|
1729
|
-
color: var(--text-secondary);
|
|
1730
|
-
padding: 6px 12px;
|
|
1731
|
-
border-radius: 6px;
|
|
1732
|
-
cursor: pointer;
|
|
1733
|
-
font-family: inherit;
|
|
1734
|
-
font-size: 12px;
|
|
1735
|
-
display: flex;
|
|
1736
|
-
align-items: center;
|
|
1737
|
-
gap: 6px;
|
|
1738
|
-
}
|
|
1739
|
-
.settings-trigger:hover {
|
|
1740
|
-
background: var(--bg-tertiary);
|
|
1741
|
-
color: var(--text-primary);
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
/* Responsive */
|
|
1745
|
-
@media (max-width: 768px) {
|
|
1746
|
-
.header { padding: 10px 14px; }
|
|
1747
|
-
.header-info { display: none; }
|
|
1748
|
-
.main { padding: 12px; }
|
|
1749
|
-
.message { max-width: 95%; }
|
|
1750
|
-
.settings-panel { width: 95%; }
|
|
1751
|
-
}
|
|
1752
|
-
</style>
|
|
1753
|
-
</head>
|
|
1754
|
-
<body>
|
|
1755
|
-
<header class="header">
|
|
1756
|
-
<div class="logo">
|
|
1757
|
-
<div class="logo-icon">X</div>
|
|
1758
|
-
<div class="logo-text">Xibe<span>Code</span></div>
|
|
1759
|
-
</div>
|
|
1760
|
-
<div class="header-info">
|
|
1761
|
-
<div class="info-item">
|
|
1762
|
-
<span class="label">Path:</span>
|
|
1763
|
-
<span class="value" id="current-path">~</span>
|
|
1764
|
-
</div>
|
|
1765
|
-
<div class="info-item">
|
|
1766
|
-
<span class="label">Model:</span>
|
|
1767
|
-
<span class="value" id="current-model">-</span>
|
|
1768
|
-
</div>
|
|
1769
|
-
<div class="info-item">
|
|
1770
|
-
<span class="label">Mode:</span>
|
|
1771
|
-
<span class="value" id="current-mode">Agent</span>
|
|
1772
|
-
</div>
|
|
1773
|
-
<div class="info-item">
|
|
1774
|
-
<span class="status-dot" id="status-dot"></span>
|
|
1775
|
-
<span id="status-text">Connecting...</span>
|
|
1776
|
-
</div>
|
|
1777
|
-
</div>
|
|
1778
|
-
<button class="settings-trigger" onclick="openSettings()">
|
|
1779
|
-
<span>⚙</span> Settings
|
|
1780
|
-
</button>
|
|
1781
|
-
</header>
|
|
1782
|
-
|
|
1783
|
-
<main class="main">
|
|
1784
|
-
<div class="messages" id="messages">
|
|
1785
|
-
<div class="message system">Welcome to XibeCode. Type <span class="hint-key">/</span> for modes or <span class="hint-key">@</span> to reference files.</div>
|
|
1786
|
-
</div>
|
|
1787
|
-
|
|
1788
|
-
<div class="thinking" id="thinking">
|
|
1789
|
-
<div class="spinner"></div>
|
|
1790
|
-
<span id="thinking-text">AI is thinking...</span>
|
|
1791
|
-
</div>
|
|
1792
|
-
|
|
1793
|
-
<div class="input-area">
|
|
1794
|
-
<div class="cmd-popup" id="cmd-popup">
|
|
1795
|
-
<div class="cmd-popup-header">Select Mode</div>
|
|
1796
|
-
<div id="cmd-list"></div>
|
|
1797
|
-
</div>
|
|
1798
|
-
<div class="cmd-popup" id="file-popup">
|
|
1799
|
-
<div class="cmd-popup-header">Select File</div>
|
|
1800
|
-
<div id="file-list"></div>
|
|
1801
|
-
</div>
|
|
1802
|
-
<div class="input-wrapper">
|
|
1803
|
-
<textarea class="input-field" id="user-input" placeholder="Type a message... (/ for modes, @ for files)" rows="1"></textarea>
|
|
1804
|
-
<button class="send-btn" id="send-btn" onclick="sendMessage()">Send</button>
|
|
1805
|
-
</div>
|
|
1806
|
-
<div class="input-hints">
|
|
1807
|
-
<span><span class="hint-key">/</span> modes</span>
|
|
1808
|
-
<span><span class="hint-key">@</span> files</span>
|
|
1809
|
-
<span><span class="hint-key">Enter</span> send</span>
|
|
1810
|
-
<span><span class="hint-key">Shift+Enter</span> new line</span>
|
|
1811
|
-
</div>
|
|
1812
|
-
</div>
|
|
1813
|
-
</main>
|
|
1814
|
-
|
|
1815
|
-
<!-- Settings Panel -->
|
|
1816
|
-
<div class="settings-overlay" id="settings-overlay" onclick="closeSettings(event)">
|
|
1817
|
-
<div class="settings-panel" onclick="event.stopPropagation()">
|
|
1818
|
-
<div class="settings-header">
|
|
1819
|
-
<h2>Settings</h2>
|
|
1820
|
-
<button class="settings-close" onclick="closeSettings()">×</button>
|
|
1821
|
-
</div>
|
|
1822
|
-
<div class="settings-content">
|
|
1823
|
-
<div class="settings-section">
|
|
1824
|
-
<h3>AI Provider</h3>
|
|
1825
|
-
<div class="settings-field">
|
|
1826
|
-
<label>Provider</label>
|
|
1827
|
-
<select id="settings-provider" onchange="onProviderChange()">
|
|
1828
|
-
<option value="anthropic">Anthropic (Claude)</option>
|
|
1829
|
-
<option value="openai">OpenAI</option>
|
|
1830
|
-
<option value="custom">Custom / OpenAI-compatible</option>
|
|
1831
|
-
</select>
|
|
1832
|
-
</div>
|
|
1833
|
-
<div class="settings-field">
|
|
1834
|
-
<label>Model</label>
|
|
1835
|
-
<select id="settings-model">
|
|
1836
|
-
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
|
|
1837
|
-
<option value="claude-opus-4-5-20251101">Claude Opus 4.5</option>
|
|
1838
|
-
<option value="claude-haiku-4-5-20251015">Claude Haiku 4.5</option>
|
|
1839
|
-
</select>
|
|
1840
|
-
</div>
|
|
1841
|
-
<div class="settings-field" id="custom-model-field" style="display:none;">
|
|
1842
|
-
<label>Custom Model ID</label>
|
|
1843
|
-
<input type="text" id="settings-custom-model" placeholder="e.g., gpt-4-turbo, llama-3-70b">
|
|
1844
|
-
<small>Enter the model identifier for your provider</small>
|
|
1845
|
-
</div>
|
|
1846
|
-
<div class="settings-field">
|
|
1847
|
-
<label>API Key</label>
|
|
1848
|
-
<input type="password" id="settings-api-key" placeholder="sk-...">
|
|
1849
|
-
</div>
|
|
1850
|
-
<div class="settings-field" id="base-url-field" style="display:none;">
|
|
1851
|
-
<label>Base URL</label>
|
|
1852
|
-
<input type="text" id="settings-base-url" placeholder="https://api.openai.com/v1">
|
|
1853
|
-
<small>For custom OpenAI-compatible endpoints</small>
|
|
1854
|
-
</div>
|
|
1855
|
-
</div>
|
|
1856
|
-
|
|
1857
|
-
<div class="settings-section">
|
|
1858
|
-
<h3>Session Info</h3>
|
|
1859
|
-
<div class="settings-field">
|
|
1860
|
-
<label>Working Directory</label>
|
|
1861
|
-
<input type="text" id="settings-workdir" readonly>
|
|
1862
|
-
</div>
|
|
1863
|
-
<div class="settings-field">
|
|
1864
|
-
<label>Git Branch</label>
|
|
1865
|
-
<input type="text" id="settings-branch" readonly>
|
|
1866
|
-
</div>
|
|
1867
|
-
<div class="settings-field">
|
|
1868
|
-
<label>Session ID</label>
|
|
1869
|
-
<input type="text" id="settings-session" readonly>
|
|
1870
|
-
</div>
|
|
1871
|
-
</div>
|
|
1872
|
-
|
|
1873
|
-
<button class="settings-btn" onclick="saveSettings()">Save Settings</button>
|
|
1874
|
-
</div>
|
|
1875
|
-
</div>
|
|
1876
|
-
</div>
|
|
1877
|
-
|
|
1878
|
-
<script>
|
|
1879
|
-
// Commands configuration
|
|
1880
|
-
const COMMANDS = [
|
|
1881
|
-
{ id: 'clear', name: '/clear', icon: '🧹', desc: 'Clear chat messages', color: '#8B949E', type: 'action' },
|
|
1882
|
-
{ id: 'help', name: '/help', icon: '❓', desc: 'Show available commands', color: '#58A6FF', type: 'action' },
|
|
1883
|
-
{ id: 'diff', name: '/diff', icon: '📝', desc: 'Show git diff', color: '#3FB950', type: 'action' },
|
|
1884
|
-
{ id: 'status', name: '/status', icon: '📊', desc: 'Show git status', color: '#A371F7', type: 'action' },
|
|
1885
|
-
{ id: 'test', name: '/test', icon: '🧪', desc: 'Run project tests', color: '#FF4081', type: 'action' },
|
|
1886
|
-
{ id: 'format', name: '/format', icon: '✨', desc: 'Format code in project', color: '#FFD740', type: 'action' },
|
|
1887
|
-
{ id: 'reset', name: '/reset', icon: '🔄', desc: 'Reset chat session', color: '#F85149', type: 'action' },
|
|
1888
|
-
{ id: 'files', name: '/files', icon: '📁', desc: 'List project files', color: '#39C5CF', type: 'action' },
|
|
1889
|
-
];
|
|
1890
|
-
|
|
1891
|
-
// Modes configuration
|
|
1892
|
-
const MODES = [
|
|
1893
|
-
{ id: 'agent', name: 'Agent', icon: '🤖', desc: 'Autonomous coding', color: '#00E676' },
|
|
1894
|
-
{ id: 'plan', name: 'Plan', icon: '📋', desc: 'Analyze and plan without modifying', color: '#40C4FF' },
|
|
1895
|
-
{ id: 'review', name: 'Review', icon: '👀', desc: 'Code review', color: '#BB86FC' },
|
|
1896
|
-
];
|
|
1897
|
-
|
|
1898
|
-
// Combined list for slash popup
|
|
1899
|
-
const ALL_SLASH_ITEMS = [
|
|
1900
|
-
...COMMANDS.map(c => ({ ...c, category: 'command' })),
|
|
1901
|
-
...MODES.map(m => ({ ...m, name: '/mode ' + m.id, category: 'mode' })),
|
|
1902
|
-
];
|
|
1903
|
-
|
|
1904
|
-
let ws = null;
|
|
1905
|
-
let files = [];
|
|
1906
|
-
let selectedCmdIndex = 0;
|
|
1907
|
-
let selectedFileIndex = 0;
|
|
1908
|
-
let currentPopup = null; // 'modes' | 'files' | null
|
|
1909
|
-
let streamingMessageEl = null;
|
|
1910
|
-
let streamingText = ''; // Track raw text for streaming
|
|
1911
|
-
|
|
1912
|
-
// Initialize
|
|
1913
|
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
1914
|
-
await loadProjectInfo();
|
|
1915
|
-
await loadConfig();
|
|
1916
|
-
connectWebSocket();
|
|
1917
|
-
setupInput();
|
|
1918
|
-
});
|
|
1919
|
-
|
|
1920
|
-
// Setup input handling
|
|
1921
|
-
function setupInput() {
|
|
1922
|
-
const input = document.getElementById('user-input');
|
|
1923
|
-
|
|
1924
|
-
input.addEventListener('input', (e) => {
|
|
1925
|
-
autoResize(input);
|
|
1926
|
-
handleInputChange(e.target.value);
|
|
1927
|
-
});
|
|
1928
|
-
|
|
1929
|
-
input.addEventListener('keydown', (e) => {
|
|
1930
|
-
if (currentPopup) {
|
|
1931
|
-
if (e.key === 'ArrowDown') {
|
|
1932
|
-
e.preventDefault();
|
|
1933
|
-
navigatePopup(1);
|
|
1934
|
-
} else if (e.key === 'ArrowUp') {
|
|
1935
|
-
e.preventDefault();
|
|
1936
|
-
navigatePopup(-1);
|
|
1937
|
-
} else if (e.key === 'Enter' && !e.shiftKey) {
|
|
1938
|
-
e.preventDefault();
|
|
1939
|
-
selectPopupItem();
|
|
1940
|
-
} else if (e.key === 'Escape') {
|
|
1941
|
-
closePopups();
|
|
1942
|
-
}
|
|
1943
|
-
} else if (e.key === 'Enter' && !e.shiftKey) {
|
|
1944
|
-
e.preventDefault();
|
|
1945
|
-
sendMessage();
|
|
1946
|
-
}
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
function autoResize(textarea) {
|
|
1951
|
-
textarea.style.height = 'auto';
|
|
1952
|
-
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
function handleInputChange(value) {
|
|
1956
|
-
const lastChar = value.slice(-1);
|
|
1957
|
-
const beforeLast = value.slice(0, -1);
|
|
1958
|
-
|
|
1959
|
-
// Check for / command at start or after space
|
|
1960
|
-
if (lastChar === '/' && (beforeLast === '' || beforeLast.endsWith(' '))) {
|
|
1961
|
-
openModePopup();
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
// Check for @ command
|
|
1966
|
-
if (lastChar === '@' && (beforeLast === '' || beforeLast.endsWith(' '))) {
|
|
1967
|
-
openFilePopup();
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
// Filter popups based on input after trigger
|
|
1972
|
-
if (currentPopup === 'modes') {
|
|
1973
|
-
const match = value.match(/\\/([\\w]*)$/);
|
|
1974
|
-
if (match) {
|
|
1975
|
-
filterSlashList(match[1]);
|
|
1976
|
-
} else {
|
|
1977
|
-
closePopups();
|
|
1978
|
-
}
|
|
1979
|
-
} else if (currentPopup === 'files') {
|
|
1980
|
-
const match = value.match(/@([\\w\\.\\-\\/]*)$/);
|
|
1981
|
-
if (match) {
|
|
1982
|
-
filterFileList(match[1]);
|
|
1983
|
-
} else {
|
|
1984
|
-
closePopups();
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
function openModePopup() {
|
|
1990
|
-
currentPopup = 'modes';
|
|
1991
|
-
selectedCmdIndex = 0;
|
|
1992
|
-
document.getElementById('cmd-popup').classList.add('visible');
|
|
1993
|
-
document.getElementById('file-popup').classList.remove('visible');
|
|
1994
|
-
renderSlashList();
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
function openFilePopup() {
|
|
1998
|
-
currentPopup = 'files';
|
|
1999
|
-
selectedFileIndex = 0;
|
|
2000
|
-
document.getElementById('file-popup').classList.add('visible');
|
|
2001
|
-
document.getElementById('cmd-popup').classList.remove('visible');
|
|
2002
|
-
loadFiles();
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
function closePopups() {
|
|
2006
|
-
currentPopup = null;
|
|
2007
|
-
document.getElementById('cmd-popup').classList.remove('visible');
|
|
2008
|
-
document.getElementById('file-popup').classList.remove('visible');
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
function renderSlashList(filter = '') {
|
|
2012
|
-
const list = document.getElementById('cmd-list');
|
|
2013
|
-
const header = document.querySelector('#cmd-popup .cmd-popup-header');
|
|
2014
|
-
header.textContent = 'Commands & Modes';
|
|
2015
|
-
|
|
2016
|
-
const filtered = ALL_SLASH_ITEMS.filter(item =>
|
|
2017
|
-
item.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
2018
|
-
item.id.toLowerCase().includes(filter.toLowerCase()) ||
|
|
2019
|
-
item.desc.toLowerCase().includes(filter.toLowerCase())
|
|
2020
|
-
);
|
|
2021
|
-
|
|
2022
|
-
if (filtered.length === 0) {
|
|
2023
|
-
list.innerHTML = '<div class="cmd-item"><div class="cmd-item-info"><div class="cmd-item-desc">No commands found</div></div></div>';
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// Group by category
|
|
2028
|
-
const commands = filtered.filter(i => i.category === 'command');
|
|
2029
|
-
const modes = filtered.filter(i => i.category === 'mode');
|
|
2030
|
-
|
|
2031
|
-
let html = '';
|
|
2032
|
-
|
|
2033
|
-
if (commands.length > 0) {
|
|
2034
|
-
html += '<div class="cmd-section-header">Commands</div>';
|
|
2035
|
-
commands.forEach((item, i) => {
|
|
2036
|
-
const globalIdx = filtered.indexOf(item);
|
|
2037
|
-
html += \`
|
|
2038
|
-
<div class="cmd-item \${globalIdx === selectedCmdIndex ? 'selected' : ''}"
|
|
2039
|
-
onclick="executeSlashItem('\${item.id}', '\${item.category}')"
|
|
2040
|
-
onmouseenter="selectedCmdIndex = \${globalIdx}; renderSlashList('\${filter}')">
|
|
2041
|
-
<div class="cmd-item-icon">\${item.icon}</div>
|
|
2042
|
-
<div class="cmd-item-info">
|
|
2043
|
-
<div class="cmd-item-name">\${item.name}</div>
|
|
2044
|
-
<div class="cmd-item-desc">\${item.desc}</div>
|
|
2045
|
-
</div>
|
|
2046
|
-
<div class="cmd-item-color" style="background: \${item.color}"></div>
|
|
2047
|
-
</div>
|
|
2048
|
-
\`;
|
|
2049
|
-
});
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
if (modes.length > 0) {
|
|
2053
|
-
html += '<div class="cmd-section-header">Modes</div>';
|
|
2054
|
-
modes.forEach((item, i) => {
|
|
2055
|
-
const globalIdx = filtered.indexOf(item);
|
|
2056
|
-
html += \`
|
|
2057
|
-
<div class="cmd-item \${globalIdx === selectedCmdIndex ? 'selected' : ''}"
|
|
2058
|
-
onclick="executeSlashItem('\${item.id}', '\${item.category}')"
|
|
2059
|
-
onmouseenter="selectedCmdIndex = \${globalIdx}; renderSlashList('\${filter}')">
|
|
2060
|
-
<div class="cmd-item-icon">\${item.icon}</div>
|
|
2061
|
-
<div class="cmd-item-info">
|
|
2062
|
-
<div class="cmd-item-name">\${item.name}</div>
|
|
2063
|
-
<div class="cmd-item-desc">\${item.desc}</div>
|
|
2064
|
-
</div>
|
|
2065
|
-
<div class="cmd-item-color" style="background: \${item.color}"></div>
|
|
2066
|
-
</div>
|
|
2067
|
-
\`;
|
|
2068
|
-
});
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
list.innerHTML = html;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
function filterSlashList(filter) {
|
|
2075
|
-
selectedCmdIndex = 0;
|
|
2076
|
-
renderSlashList(filter);
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
async function loadFiles() {
|
|
2080
|
-
try {
|
|
2081
|
-
const res = await fetch('/api/files');
|
|
2082
|
-
const data = await res.json();
|
|
2083
|
-
files = data.files || [];
|
|
2084
|
-
renderFileList();
|
|
2085
|
-
} catch (e) {
|
|
2086
|
-
files = [];
|
|
2087
|
-
renderFileList();
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
function renderFileList(filter = '') {
|
|
2092
|
-
const list = document.getElementById('file-list');
|
|
2093
|
-
const filtered = files.filter(f =>
|
|
2094
|
-
f.toLowerCase().includes(filter.toLowerCase())
|
|
2095
|
-
).slice(0, 20);
|
|
2096
|
-
|
|
2097
|
-
if (filtered.length === 0) {
|
|
2098
|
-
list.innerHTML = '<div class="cmd-item"><div class="cmd-item-info"><div class="cmd-item-desc">No files found</div></div></div>';
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
list.innerHTML = filtered.map((file, i) => \`
|
|
2103
|
-
<div class="cmd-item \${i === selectedFileIndex ? 'selected' : ''}"
|
|
2104
|
-
onclick="selectFile('\${file}')"
|
|
2105
|
-
onmouseenter="selectedFileIndex = \${i}; renderFileList('\${filter}')">
|
|
2106
|
-
<div class="cmd-item-icon">\${file.includes('.') ? '📄' : '📁'}</div>
|
|
2107
|
-
<div class="cmd-item-info">
|
|
2108
|
-
<div class="cmd-item-name">\${file.split('/').pop()}</div>
|
|
2109
|
-
<div class="cmd-item-desc">\${file}</div>
|
|
2110
|
-
</div>
|
|
2111
|
-
</div>
|
|
2112
|
-
\`).join('');
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
function filterFileList(filter) {
|
|
2116
|
-
selectedFileIndex = 0;
|
|
2117
|
-
renderFileList(filter);
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
function navigatePopup(direction) {
|
|
2121
|
-
if (currentPopup === 'modes') {
|
|
2122
|
-
const input = document.getElementById('user-input').value;
|
|
2123
|
-
const match = input.match(/\\/([\\w]*)$/);
|
|
2124
|
-
const filter = match ? match[1] : '';
|
|
2125
|
-
const filtered = ALL_SLASH_ITEMS.filter(item =>
|
|
2126
|
-
item.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
2127
|
-
item.id.toLowerCase().includes(filter.toLowerCase())
|
|
2128
|
-
);
|
|
2129
|
-
selectedCmdIndex = Math.max(0, Math.min(filtered.length - 1, selectedCmdIndex + direction));
|
|
2130
|
-
renderSlashList(filter);
|
|
2131
|
-
} else if (currentPopup === 'files') {
|
|
2132
|
-
const input = document.getElementById('user-input').value;
|
|
2133
|
-
const match = input.match(/@([\\w\\.\\-\\/]*)$/);
|
|
2134
|
-
const filter = match ? match[1] : '';
|
|
2135
|
-
const filteredFiles = files.filter(f =>
|
|
2136
|
-
f.toLowerCase().includes(filter.toLowerCase())
|
|
2137
|
-
).slice(0, 20);
|
|
2138
|
-
selectedFileIndex = Math.max(0, Math.min(filteredFiles.length - 1, selectedFileIndex + direction));
|
|
2139
|
-
renderFileList(filter);
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
function selectPopupItem() {
|
|
2144
|
-
if (currentPopup === 'modes') {
|
|
2145
|
-
const input = document.getElementById('user-input').value;
|
|
2146
|
-
const match = input.match(/\\/([\\w]*)$/);
|
|
2147
|
-
const filter = match ? match[1] : '';
|
|
2148
|
-
const filtered = ALL_SLASH_ITEMS.filter(item =>
|
|
2149
|
-
item.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
2150
|
-
item.id.toLowerCase().includes(filter.toLowerCase())
|
|
2151
|
-
);
|
|
2152
|
-
if (filtered[selectedCmdIndex]) {
|
|
2153
|
-
executeSlashItem(filtered[selectedCmdIndex].id, filtered[selectedCmdIndex].category);
|
|
2154
|
-
}
|
|
2155
|
-
} else if (currentPopup === 'files') {
|
|
2156
|
-
const input = document.getElementById('user-input').value;
|
|
2157
|
-
const match = input.match(/@([\\w\\.\\-\\/]*)$/);
|
|
2158
|
-
const filter = match ? match[1] : '';
|
|
2159
|
-
const filteredFiles = files.filter(f =>
|
|
2160
|
-
f.toLowerCase().includes(filter.toLowerCase())
|
|
2161
|
-
).slice(0, 20);
|
|
2162
|
-
if (filteredFiles[selectedFileIndex]) {
|
|
2163
|
-
selectFile(filteredFiles[selectedFileIndex]);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
let currentMode = 'agent';
|
|
2169
|
-
|
|
2170
|
-
async function executeSlashItem(itemId, category) {
|
|
2171
|
-
const input = document.getElementById('user-input');
|
|
2172
|
-
input.value = ''; // Clear input
|
|
2173
|
-
closePopups();
|
|
2174
|
-
input.focus();
|
|
2175
|
-
|
|
2176
|
-
if (category === 'mode') {
|
|
2177
|
-
// Switch mode
|
|
2178
|
-
currentMode = itemId;
|
|
2179
|
-
const mode = MODES.find(m => m.id === itemId);
|
|
2180
|
-
document.getElementById('current-mode').textContent = mode?.name || itemId;
|
|
2181
|
-
addMessage('system', \`Switched to \${mode?.icon || ''} \${mode?.name || itemId} mode\`);
|
|
2182
|
-
|
|
2183
|
-
// Send mode switch command to TUI
|
|
2184
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2185
|
-
ws.send(JSON.stringify({ type: 'message', content: '/mode ' + itemId }));
|
|
2186
|
-
}
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// Execute command
|
|
2191
|
-
switch (itemId) {
|
|
2192
|
-
case 'clear':
|
|
2193
|
-
document.getElementById('messages').innerHTML = '<div class="message system">Chat cleared. Type <span class="hint-key">/</span> for commands.</div>';
|
|
2194
|
-
break;
|
|
2195
|
-
|
|
2196
|
-
case 'help':
|
|
2197
|
-
const helpText = \`**Available Commands:**
|
|
2198
|
-
- **/clear** - Clear chat messages
|
|
2199
|
-
- **/help** - Show this help
|
|
2200
|
-
- **/diff** - Show git diff
|
|
2201
|
-
- **/status** - Show git status
|
|
2202
|
-
- **/test** - Run project tests
|
|
2203
|
-
- **/format** - Format code
|
|
2204
|
-
- **/reset** - Reset session
|
|
2205
|
-
- **/files** - List project files
|
|
2206
|
-
- **/mode [name]** - Switch agent mode
|
|
2207
|
-
|
|
2208
|
-
**Available Modes:** agent, plan, review
|
|
2209
|
-
|
|
2210
|
-
**Tips:**
|
|
2211
|
-
- Type **@** to reference files
|
|
2212
|
-
- Press **Enter** to send, **Shift+Enter** for new line\`;
|
|
2213
|
-
addMessage('assistant', helpText);
|
|
2214
|
-
break;
|
|
2215
|
-
|
|
2216
|
-
case 'diff':
|
|
2217
|
-
showThinking(true, 'Getting git diff...');
|
|
2218
|
-
try {
|
|
2219
|
-
const diffRes = await fetch('/api/git/diff');
|
|
2220
|
-
const diffData = await diffRes.json();
|
|
2221
|
-
showThinking(false);
|
|
2222
|
-
if (diffData.success && diffData.diff) {
|
|
2223
|
-
addMessage('assistant', '**Git Diff:**\\n\`\`\`diff\\n' + diffData.diff + '\\n\`\`\`');
|
|
2224
|
-
} else {
|
|
2225
|
-
addMessage('system', 'No changes to show or not a git repository.');
|
|
2226
|
-
}
|
|
2227
|
-
} catch (e) {
|
|
2228
|
-
showThinking(false);
|
|
2229
|
-
addMessage('system', 'Failed to get git diff');
|
|
2230
|
-
}
|
|
2231
|
-
break;
|
|
2232
|
-
|
|
2233
|
-
case 'status':
|
|
2234
|
-
showThinking(true, 'Getting git status...');
|
|
2235
|
-
try {
|
|
2236
|
-
const statusRes = await fetch('/api/git/status');
|
|
2237
|
-
const statusData = await statusRes.json();
|
|
2238
|
-
showThinking(false);
|
|
2239
|
-
if (statusData.success) {
|
|
2240
|
-
const statusMsg = \`**Git Status:**
|
|
2241
|
-
- Branch: **\${statusData.branch || 'unknown'}**
|
|
2242
|
-
- Status: \${statusData.clean ? '✅ Clean' : '⚠️ Uncommitted changes'}
|
|
2243
|
-
- Modified: \${statusData.modifiedCount || 0} files
|
|
2244
|
-
- Staged: \${statusData.stagedCount || 0} files
|
|
2245
|
-
- Untracked: \${statusData.untrackedCount || 0} files\`;
|
|
2246
|
-
addMessage('assistant', statusMsg);
|
|
2247
|
-
} else {
|
|
2248
|
-
addMessage('system', 'Not a git repository or git not available.');
|
|
2249
|
-
}
|
|
2250
|
-
} catch (e) {
|
|
2251
|
-
showThinking(false);
|
|
2252
|
-
addMessage('system', 'Failed to get git status');
|
|
2253
|
-
}
|
|
2254
|
-
break;
|
|
2255
|
-
|
|
2256
|
-
case 'test':
|
|
2257
|
-
addMessage('system', 'Running tests...');
|
|
2258
|
-
// Send to TUI to run tests
|
|
2259
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2260
|
-
ws.send(JSON.stringify({ type: 'message', content: 'Run the project tests' }));
|
|
2261
|
-
}
|
|
2262
|
-
break;
|
|
2263
|
-
|
|
2264
|
-
case 'format':
|
|
2265
|
-
addMessage('system', 'Formatting code...');
|
|
2266
|
-
// Send to TUI to format
|
|
2267
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2268
|
-
ws.send(JSON.stringify({ type: 'message', content: 'Format the code in this project using the appropriate formatter' }));
|
|
2269
|
-
}
|
|
2270
|
-
break;
|
|
2271
|
-
|
|
2272
|
-
case 'reset':
|
|
2273
|
-
document.getElementById('messages').innerHTML = '<div class="message system">Session reset. Welcome to XibeCode.</div>';
|
|
2274
|
-
currentMode = 'agent';
|
|
2275
|
-
document.getElementById('current-mode').textContent = 'Agent';
|
|
2276
|
-
break;
|
|
2277
|
-
|
|
2278
|
-
case 'files':
|
|
2279
|
-
showThinking(true, 'Listing files...');
|
|
2280
|
-
try {
|
|
2281
|
-
const filesRes = await fetch('/api/files');
|
|
2282
|
-
const filesData = await filesRes.json();
|
|
2283
|
-
showThinking(false);
|
|
2284
|
-
if (filesData.success && filesData.files) {
|
|
2285
|
-
const filesList = filesData.files.slice(0, 50).join('\\n');
|
|
2286
|
-
addMessage('assistant', \`**Project Files (\${filesData.files.length} total):**\\n\\\`\\\`\\\`\\n\${filesList}\${filesData.files.length > 50 ? '\\n... and more' : ''}\\n\\\`\\\`\\\`\`);
|
|
2287
|
-
} else {
|
|
2288
|
-
addMessage('system', 'Failed to list files.');
|
|
2289
|
-
}
|
|
2290
|
-
} catch (e) {
|
|
2291
|
-
showThinking(false);
|
|
2292
|
-
addMessage('system', 'Failed to list files');
|
|
2293
|
-
}
|
|
2294
|
-
break;
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
function selectFile(file) {
|
|
2299
|
-
const input = document.getElementById('user-input');
|
|
2300
|
-
// Replace @xxx with the file path
|
|
2301
|
-
input.value = input.value.replace(/@[\\w\\.\\-\\/]*$/, '@' + file + ' ');
|
|
2302
|
-
closePopups();
|
|
2303
|
-
input.focus();
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
// WebSocket connection
|
|
2307
|
-
function connectWebSocket() {
|
|
2308
|
-
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2309
|
-
ws = new WebSocket(\`\${protocol}//\${location.host}?mode=bridge\`);
|
|
2310
|
-
|
|
2311
|
-
ws.onopen = () => {
|
|
2312
|
-
document.getElementById('status-dot').classList.add('connected');
|
|
2313
|
-
document.getElementById('status-text').textContent = 'Connected';
|
|
2314
|
-
};
|
|
2315
|
-
|
|
2316
|
-
ws.onclose = () => {
|
|
2317
|
-
document.getElementById('status-dot').classList.remove('connected');
|
|
2318
|
-
document.getElementById('status-text').textContent = 'Disconnected';
|
|
2319
|
-
setTimeout(connectWebSocket, 3000);
|
|
2320
|
-
};
|
|
2321
|
-
|
|
2322
|
-
ws.onmessage = (event) => {
|
|
2323
|
-
const data = JSON.parse(event.data);
|
|
2324
|
-
handleWSMessage(data);
|
|
2325
|
-
};
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
function handleWSMessage(data) {
|
|
2329
|
-
switch (data.type) {
|
|
2330
|
-
case 'user_message':
|
|
2331
|
-
// Only show TUI messages - WebUI messages are already shown locally
|
|
2332
|
-
if (data.source === 'tui') {
|
|
2333
|
-
addMessage('user', data.data.content + ' (TUI)');
|
|
2334
|
-
}
|
|
2335
|
-
document.getElementById('send-btn').disabled = true;
|
|
2336
|
-
showThinking(true);
|
|
2337
|
-
break;
|
|
2338
|
-
case 'assistant_message':
|
|
2339
|
-
addMessage('assistant', data.data.content);
|
|
2340
|
-
document.getElementById('send-btn').disabled = false;
|
|
2341
|
-
showThinking(false);
|
|
2342
|
-
break;
|
|
2343
|
-
case 'thinking':
|
|
2344
|
-
showThinking(true, data.data?.text || 'Processing...');
|
|
2345
|
-
break;
|
|
2346
|
-
case 'stream_start':
|
|
2347
|
-
startStreamMessage();
|
|
2348
|
-
showThinking(false);
|
|
2349
|
-
break;
|
|
2350
|
-
case 'stream_text':
|
|
2351
|
-
appendStreamText(data.data?.text || data.text || '');
|
|
2352
|
-
break;
|
|
2353
|
-
case 'stream_end':
|
|
2354
|
-
endStreamMessage();
|
|
2355
|
-
document.getElementById('send-btn').disabled = false;
|
|
2356
|
-
break;
|
|
2357
|
-
case 'tool_call':
|
|
2358
|
-
addToolMessage(data.data?.name || data.name, 'running');
|
|
2359
|
-
break;
|
|
2360
|
-
case 'tool_result':
|
|
2361
|
-
updateLastToolMessage(data.data?.name || data.name, data.data?.success ? 'success' : 'error');
|
|
2362
|
-
break;
|
|
2363
|
-
case 'complete':
|
|
2364
|
-
document.getElementById('send-btn').disabled = false;
|
|
2365
|
-
showThinking(false);
|
|
2366
|
-
break;
|
|
2367
|
-
case 'error':
|
|
2368
|
-
addMessage('system', 'Error: ' + (data.data?.error || data.error));
|
|
2369
|
-
document.getElementById('send-btn').disabled = false;
|
|
2370
|
-
showThinking(false);
|
|
2371
|
-
break;
|
|
2372
|
-
case 'session_sync':
|
|
2373
|
-
if (data.data?.sessionId) {
|
|
2374
|
-
document.getElementById('settings-session').value = data.data.sessionId;
|
|
2375
|
-
}
|
|
2376
|
-
break;
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
function showThinking(show, text = 'AI is thinking...') {
|
|
2381
|
-
const el = document.getElementById('thinking');
|
|
2382
|
-
if (show) {
|
|
2383
|
-
el.classList.add('visible');
|
|
2384
|
-
document.getElementById('thinking-text').textContent = text;
|
|
2385
|
-
} else {
|
|
2386
|
-
el.classList.remove('visible');
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
function startStreamMessage() {
|
|
2391
|
-
const messages = document.getElementById('messages');
|
|
2392
|
-
streamingMessageEl = document.createElement('div');
|
|
2393
|
-
streamingMessageEl.className = 'message assistant';
|
|
2394
|
-
streamingText = ''; // Reset streaming text
|
|
2395
|
-
messages.appendChild(streamingMessageEl);
|
|
2396
|
-
messages.scrollTop = messages.scrollHeight;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
function appendStreamText(text) {
|
|
2400
|
-
if (streamingMessageEl && text) {
|
|
2401
|
-
streamingText += text;
|
|
2402
|
-
// For performance, only render markdown every few updates or just show plain text while streaming
|
|
2403
|
-
streamingMessageEl.textContent = streamingText;
|
|
2404
|
-
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
function endStreamMessage() {
|
|
2409
|
-
if (streamingMessageEl) {
|
|
2410
|
-
// Final markdown render when streaming is complete
|
|
2411
|
-
streamingMessageEl.innerHTML = renderMarkdown(streamingText);
|
|
2412
|
-
}
|
|
2413
|
-
streamingMessageEl = null;
|
|
2414
|
-
streamingText = '';
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
function addMessage(role, content) {
|
|
2418
|
-
const messages = document.getElementById('messages');
|
|
2419
|
-
const msg = document.createElement('div');
|
|
2420
|
-
msg.className = 'message ' + role;
|
|
2421
|
-
if (role === 'assistant') {
|
|
2422
|
-
msg.innerHTML = renderMarkdown(content);
|
|
2423
|
-
} else {
|
|
2424
|
-
msg.textContent = content;
|
|
2425
|
-
}
|
|
2426
|
-
messages.appendChild(msg);
|
|
2427
|
-
messages.scrollTop = messages.scrollHeight;
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
function addToolMessage(name, status) {
|
|
2431
|
-
const messages = document.getElementById('messages');
|
|
2432
|
-
const msg = document.createElement('div');
|
|
2433
|
-
msg.className = 'message tool';
|
|
2434
|
-
msg.innerHTML = \`<span class="tool-name">\${escapeHtml(name)}</span><span class="tool-status \${status}">\${status === 'running' ? '⏳ running' : status}</span>\`;
|
|
2435
|
-
msg.dataset.toolName = name;
|
|
2436
|
-
messages.appendChild(msg);
|
|
2437
|
-
messages.scrollTop = messages.scrollHeight;
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
function updateLastToolMessage(name, status) {
|
|
2441
|
-
const messages = document.getElementById('messages');
|
|
2442
|
-
const toolMsgs = messages.querySelectorAll('.message.tool');
|
|
2443
|
-
for (let i = toolMsgs.length - 1; i >= 0; i--) {
|
|
2444
|
-
if (toolMsgs[i].dataset.toolName === name) {
|
|
2445
|
-
toolMsgs[i].innerHTML = \`<span class="tool-name">\${escapeHtml(name)}</span><span class="tool-status \${status}">\${status === 'success' ? '✓ done' : '✗ failed'}</span>\`;
|
|
2446
|
-
break;
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
function sendMessage() {
|
|
2452
|
-
const input = document.getElementById('user-input');
|
|
2453
|
-
const message = input.value.trim();
|
|
2454
|
-
if (!message) return;
|
|
2455
|
-
|
|
2456
|
-
addMessage('user', message);
|
|
2457
|
-
input.value = '';
|
|
2458
|
-
autoResize(input);
|
|
2459
|
-
document.getElementById('send-btn').disabled = true;
|
|
2460
|
-
showThinking(true);
|
|
2461
|
-
|
|
2462
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2463
|
-
ws.send(JSON.stringify({ type: 'message', content: message }));
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
// Simple markdown renderer
|
|
2468
|
-
function renderMarkdown(text) {
|
|
2469
|
-
if (!text) return '';
|
|
2470
|
-
let html = escapeHtml(text);
|
|
2471
|
-
|
|
2472
|
-
// Code blocks
|
|
2473
|
-
html = html.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, '<pre><code>$1</code></pre>');
|
|
2474
|
-
// Inline code
|
|
2475
|
-
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
2476
|
-
// Bold
|
|
2477
|
-
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
2478
|
-
// Italic
|
|
2479
|
-
html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
2480
|
-
// Headers
|
|
2481
|
-
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
2482
|
-
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
2483
|
-
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
2484
|
-
// Lists
|
|
2485
|
-
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
2486
|
-
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
|
2487
|
-
// Links
|
|
2488
|
-
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
2489
|
-
// Line breaks
|
|
2490
|
-
html = html.replace(/\\n/g, '<br>');
|
|
2491
|
-
|
|
2492
|
-
return html;
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
function escapeHtml(text) {
|
|
2496
|
-
const div = document.createElement('div');
|
|
2497
|
-
div.textContent = text;
|
|
2498
|
-
return div.innerHTML;
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
// Settings
|
|
2502
|
-
function openSettings() {
|
|
2503
|
-
document.getElementById('settings-overlay').classList.add('visible');
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
|
-
function closeSettings(event) {
|
|
2507
|
-
if (!event || event.target === event.currentTarget) {
|
|
2508
|
-
document.getElementById('settings-overlay').classList.remove('visible');
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
function onProviderChange() {
|
|
2513
|
-
const provider = document.getElementById('settings-provider').value;
|
|
2514
|
-
const modelSelect = document.getElementById('settings-model');
|
|
2515
|
-
const customField = document.getElementById('custom-model-field');
|
|
2516
|
-
const baseUrlField = document.getElementById('base-url-field');
|
|
2517
|
-
|
|
2518
|
-
if (provider === 'anthropic') {
|
|
2519
|
-
modelSelect.innerHTML = \`
|
|
2520
|
-
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
|
|
2521
|
-
<option value="claude-opus-4-5-20251101">Claude Opus 4.5</option>
|
|
2522
|
-
<option value="claude-haiku-4-5-20251015">Claude Haiku 4.5</option>
|
|
2523
|
-
\`;
|
|
2524
|
-
customField.style.display = 'none';
|
|
2525
|
-
baseUrlField.style.display = 'none';
|
|
2526
|
-
} else if (provider === 'openai') {
|
|
2527
|
-
modelSelect.innerHTML = \`
|
|
2528
|
-
<option value="gpt-4o">GPT-4o</option>
|
|
2529
|
-
<option value="gpt-4o-mini">GPT-4o Mini</option>
|
|
2530
|
-
<option value="gpt-4-turbo">GPT-4 Turbo</option>
|
|
2531
|
-
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
|
|
2532
|
-
\`;
|
|
2533
|
-
customField.style.display = 'none';
|
|
2534
|
-
baseUrlField.style.display = 'none';
|
|
2535
|
-
} else {
|
|
2536
|
-
modelSelect.innerHTML = '<option value="custom">Custom Model</option>';
|
|
2537
|
-
customField.style.display = 'block';
|
|
2538
|
-
baseUrlField.style.display = 'block';
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
async function loadProjectInfo() {
|
|
2543
|
-
try {
|
|
2544
|
-
const res = await fetch('/api/project');
|
|
2545
|
-
const info = await res.json();
|
|
2546
|
-
document.getElementById('current-path').textContent = info.workingDir || info.name || '~';
|
|
2547
|
-
document.getElementById('settings-workdir').value = info.workingDir || process.cwd();
|
|
2548
|
-
document.getElementById('settings-branch').value = info.gitBranch || 'N/A';
|
|
2549
|
-
} catch (e) {
|
|
2550
|
-
console.error('Failed to load project info:', e);
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
async function loadConfig() {
|
|
2555
|
-
try {
|
|
2556
|
-
const res = await fetch('/api/config');
|
|
2557
|
-
const config = await res.json();
|
|
2558
|
-
document.getElementById('current-model').textContent = config.currentModel?.split('-').slice(0, 2).join('-') || 'Claude';
|
|
2559
|
-
document.getElementById('settings-model').value = config.currentModel || 'claude-sonnet-4-5-20250929';
|
|
2560
|
-
if (config.apiKeySet) {
|
|
2561
|
-
document.getElementById('settings-api-key').placeholder = '••••••••';
|
|
2562
|
-
}
|
|
2563
|
-
} catch (e) {
|
|
2564
|
-
console.error('Failed to load config:', e);
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
async function saveSettings() {
|
|
2569
|
-
const provider = document.getElementById('settings-provider').value;
|
|
2570
|
-
const model = provider === 'custom'
|
|
2571
|
-
? document.getElementById('settings-custom-model').value
|
|
2572
|
-
: document.getElementById('settings-model').value;
|
|
2573
|
-
const apiKey = document.getElementById('settings-api-key').value;
|
|
2574
|
-
const baseUrl = document.getElementById('settings-base-url').value;
|
|
2575
|
-
|
|
2576
|
-
try {
|
|
2577
|
-
await fetch('/api/config', {
|
|
2578
|
-
method: 'PUT',
|
|
2579
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2580
|
-
body: JSON.stringify({
|
|
2581
|
-
model,
|
|
2582
|
-
apiKey: apiKey || undefined,
|
|
2583
|
-
baseUrl: baseUrl || undefined,
|
|
2584
|
-
provider
|
|
2585
|
-
}),
|
|
2586
|
-
});
|
|
2587
|
-
addMessage('system', 'Settings saved successfully');
|
|
2588
|
-
closeSettings();
|
|
2589
|
-
// Update header
|
|
2590
|
-
document.getElementById('current-model').textContent = model.split('-').slice(0, 2).join('-');
|
|
2591
|
-
} catch (e) {
|
|
2592
|
-
addMessage('system', 'Failed to save settings');
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
</script>
|
|
2596
|
-
</body>
|
|
2597
|
-
</html>`;
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
/**
|
|
2601
|
-
* Start WebUI server from CLI
|
|
2602
|
-
*/
|
|
2603
|
-
export async function startWebUI(options = {}) {
|
|
2604
|
-
const server = new WebUIServer(options);
|
|
2605
|
-
await server.start();
|
|
2606
|
-
return server;
|
|
2607
|
-
}
|
|
2608
|
-
//# sourceMappingURL=server.js.map
|