vg-coder-cli 2.0.25 → 2.0.26
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/.vg/tree-state.json +1 -7
- package/package.json +1 -1
- package/src/index.js +36 -0
- package/src/server/api-server.js +167 -31
- package/src/server/project-manager.js +353 -0
- package/src/server/terminal-manager.js +31 -3
- package/src/server/views/css/iframe.css +1 -0
- package/src/server/views/dashboard.css +138 -30
- package/src/server/views/dashboard.html +94 -11
- package/src/server/views/js/features/commands.js +3 -1
- package/src/server/views/js/features/iframe-manager.js +6 -2
- package/src/server/views/js/features/project-switcher.js +153 -0
- package/src/server/views/js/features/terminal.js +44 -6
- package/src/server/views/js/main.js +61 -0
- package/src/server/views/vg-coder/controller.js +376 -2
- package/vetgo-auto/chrome/src/controller.ts +75 -1
- package/vetgo-auto/chrome/src/utils/ai-domains.ts +33 -0
- package/vetgo-auto/chrome/src/utils/injector-script.ts +251 -0
- package/vetgo-auto/vg-coder.zip +0 -0
- package/vg-coder-cli-2.0.26.tgz +0 -0
- package/vg-coder-cli-2.0.23.tgz +0 -0
- package/vg-coder-cli-2.0.24.tgz +0 -0
- package/vg-coder-cli-2.0.25.tgz +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ProjectManager - Singleton for managing multiple projects in one server
|
|
10
|
+
*/
|
|
11
|
+
class ProjectManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
if (ProjectManager.instance) {
|
|
14
|
+
return ProjectManager.instance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.projects = new Map(); // projectId -> ProjectContext
|
|
18
|
+
this.currentProjectId = null;
|
|
19
|
+
this.lockFilePath = path.join(os.homedir(), '.vg', 'vg-leader.lock');
|
|
20
|
+
|
|
21
|
+
ProjectManager.instance = this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a new project
|
|
26
|
+
* @param {string} workingDir - Absolute path to project directory
|
|
27
|
+
* @returns {string} projectId
|
|
28
|
+
*/
|
|
29
|
+
registerProject(workingDir) {
|
|
30
|
+
const resolvedPath = path.resolve(workingDir);
|
|
31
|
+
const projectId = this.generateProjectId(resolvedPath);
|
|
32
|
+
|
|
33
|
+
// Check if already registered
|
|
34
|
+
if (this.projects.has(projectId)) {
|
|
35
|
+
console.log(chalk.yellow(`⚠️ Project already registered: ${path.basename(resolvedPath)}`));
|
|
36
|
+
return projectId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projectContext = {
|
|
40
|
+
id: projectId,
|
|
41
|
+
name: path.basename(resolvedPath),
|
|
42
|
+
workingDir: resolvedPath,
|
|
43
|
+
active: this.projects.size === 0, // First project is active by default
|
|
44
|
+
createdAt: Date.now(),
|
|
45
|
+
lastActive: Date.now()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.projects.set(projectId, projectContext);
|
|
49
|
+
|
|
50
|
+
// Set as current if first project
|
|
51
|
+
if (this.projects.size === 1) {
|
|
52
|
+
this.currentProjectId = projectId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(chalk.green(`✓ Registered project: ${chalk.cyan(projectContext.name)} (${this.projects.size} total)`));
|
|
56
|
+
|
|
57
|
+
return projectId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Switch to a different project
|
|
62
|
+
* @param {string} projectId
|
|
63
|
+
* @returns {boolean} success
|
|
64
|
+
*/
|
|
65
|
+
switchProject(projectId) {
|
|
66
|
+
if (!this.projects.has(projectId)) {
|
|
67
|
+
console.error(chalk.red(`❌ Project not found: ${projectId}`));
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Deactivate current project
|
|
72
|
+
if (this.currentProjectId) {
|
|
73
|
+
const current = this.projects.get(this.currentProjectId);
|
|
74
|
+
if (current) {
|
|
75
|
+
current.active = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Activate new project
|
|
80
|
+
const newProject = this.projects.get(projectId);
|
|
81
|
+
newProject.active = true;
|
|
82
|
+
newProject.lastActive = Date.now();
|
|
83
|
+
this.currentProjectId = projectId;
|
|
84
|
+
|
|
85
|
+
console.log(chalk.blue(`Switched to project: ${chalk.cyan(newProject.name)}`));
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get currently active project
|
|
92
|
+
* @returns {ProjectContext|null}
|
|
93
|
+
*/
|
|
94
|
+
getActiveProject() {
|
|
95
|
+
if (!this.currentProjectId) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return this.projects.get(this.currentProjectId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all projects
|
|
103
|
+
* @returns {Array<ProjectContext>}
|
|
104
|
+
*/
|
|
105
|
+
getAllProjects() {
|
|
106
|
+
return Array.from(this.projects.values())
|
|
107
|
+
.sort((a, b) => b.lastActive - a.lastActive); // Sort by last active
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove a project
|
|
112
|
+
* @param {string} projectId
|
|
113
|
+
*/
|
|
114
|
+
removeProject(projectId) {
|
|
115
|
+
const project = this.projects.get(projectId);
|
|
116
|
+
if (!project) return;
|
|
117
|
+
|
|
118
|
+
this.projects.delete(projectId);
|
|
119
|
+
console.log(chalk.yellow(`Removed project: ${project.name}`));
|
|
120
|
+
|
|
121
|
+
// If removed project was active, switch to another
|
|
122
|
+
if (projectId === this.currentProjectId) {
|
|
123
|
+
const remaining = Array.from(this.projects.keys());
|
|
124
|
+
if (remaining.length > 0) {
|
|
125
|
+
this.switchProject(remaining[0]);
|
|
126
|
+
} else {
|
|
127
|
+
this.currentProjectId = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate unique project ID from path
|
|
134
|
+
* @param {string} projectPath
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
generateProjectId(projectPath) {
|
|
138
|
+
return crypto
|
|
139
|
+
.createHash('md5')
|
|
140
|
+
.update(projectPath)
|
|
141
|
+
.digest('hex')
|
|
142
|
+
.substring(0, 8);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Acquire leader lock
|
|
147
|
+
* @param {number} port
|
|
148
|
+
* @param {number} pid
|
|
149
|
+
* @returns {Promise<boolean>}
|
|
150
|
+
*/
|
|
151
|
+
async acquireLock(port, pid = process.pid) {
|
|
152
|
+
try {
|
|
153
|
+
// Ensure .vg directory exists
|
|
154
|
+
await fs.ensureDir(path.dirname(this.lockFilePath));
|
|
155
|
+
|
|
156
|
+
// Check if lock already exists
|
|
157
|
+
if (await fs.pathExists(this.lockFilePath)) {
|
|
158
|
+
const existingLock = await fs.readJson(this.lockFilePath);
|
|
159
|
+
|
|
160
|
+
// Check if existing leader is still alive
|
|
161
|
+
const isAlive = await this.checkLeaderHealth(existingLock.port);
|
|
162
|
+
if (isAlive) {
|
|
163
|
+
console.log(chalk.yellow('⚠️ Leader already exists'));
|
|
164
|
+
return false;
|
|
165
|
+
} else {
|
|
166
|
+
console.log(chalk.yellow('⚠️ Stale lock file detected, cleaning up...'));
|
|
167
|
+
await fs.remove(this.lockFilePath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Write lock file
|
|
172
|
+
const lockData = {
|
|
173
|
+
port,
|
|
174
|
+
pid,
|
|
175
|
+
startTime: Date.now(),
|
|
176
|
+
hostname: os.hostname()
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await fs.writeJson(this.lockFilePath, lockData, { spaces: 2 });
|
|
180
|
+
console.log(chalk.green(`✓ Acquired leader lock (PID: ${pid}, Port: ${port})`));
|
|
181
|
+
|
|
182
|
+
return true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(chalk.red('❌ Failed to acquire lock:'), error.message);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Release leader lock
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
async releaseLock() {
|
|
194
|
+
try {
|
|
195
|
+
if (await fs.pathExists(this.lockFilePath)) {
|
|
196
|
+
await fs.remove(this.lockFilePath);
|
|
197
|
+
console.log(chalk.green('✓ Released leader lock'));
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(chalk.red('❌ Failed to release lock:'), error.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if leader exists and is healthy
|
|
206
|
+
* @returns {Promise<Object|null>} Leader info or null
|
|
207
|
+
*/
|
|
208
|
+
async checkLeader() {
|
|
209
|
+
try {
|
|
210
|
+
if (!await fs.pathExists(this.lockFilePath)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lockData = await fs.readJson(this.lockFilePath);
|
|
215
|
+
|
|
216
|
+
// Check if leader is healthy
|
|
217
|
+
const isHealthy = await this.checkLeaderHealth(lockData.port);
|
|
218
|
+
|
|
219
|
+
if (isHealthy) {
|
|
220
|
+
return lockData;
|
|
221
|
+
} else {
|
|
222
|
+
// Stale lock file
|
|
223
|
+
console.log(chalk.yellow('⚠️ Leader not responding, cleaning up stale lock...'));
|
|
224
|
+
await fs.remove(this.lockFilePath);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(chalk.red('❌ Error checking leader:'), error.message);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if leader server is healthy
|
|
235
|
+
* @param {number} port
|
|
236
|
+
* @returns {Promise<boolean>}
|
|
237
|
+
*/
|
|
238
|
+
async checkLeaderHealth(port) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
const req = http.get(`http://localhost:${port}/health`, (res) => {
|
|
241
|
+
resolve(res.statusCode === 200);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
req.on('error', () => {
|
|
245
|
+
resolve(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
req.setTimeout(2000, () => {
|
|
249
|
+
req.destroy();
|
|
250
|
+
resolve(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Join existing leader as follower
|
|
257
|
+
* @param {Object} leaderInfo
|
|
258
|
+
* @param {string} projectPath
|
|
259
|
+
* @returns {Promise<boolean>}
|
|
260
|
+
*/
|
|
261
|
+
async joinLeader(leaderInfo, projectPath) {
|
|
262
|
+
try {
|
|
263
|
+
const { port } = leaderInfo;
|
|
264
|
+
const resolvedPath = path.resolve(projectPath);
|
|
265
|
+
const projectName = path.basename(resolvedPath);
|
|
266
|
+
|
|
267
|
+
console.log(chalk.blue(`\n📡 Joining leader at port ${port}...`));
|
|
268
|
+
|
|
269
|
+
// Send registration request to leader
|
|
270
|
+
const response = await this.sendHttpRequest(port, '/api/projects/register', {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
workingDir: resolvedPath,
|
|
274
|
+
name: projectName
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (response.success) {
|
|
279
|
+
console.log(chalk.green('\n──────────────────────────────────────────────────'));
|
|
280
|
+
console.log(`🔗 ${chalk.bold('Project Registered')} ${chalk.green('● Joined')}`);
|
|
281
|
+
console.log(chalk.gray('──────────────────────────────────────────────────'));
|
|
282
|
+
console.log(`📁 Project: ${chalk.cyan(projectName)}`);
|
|
283
|
+
console.log(`🌐 Dashboard: ${chalk.blue(`http://localhost:${port}`)}`);
|
|
284
|
+
console.log(`📊 Total: ${chalk.yellow(response.totalProjects)} project(s)`);
|
|
285
|
+
console.log(chalk.green('──────────────────────────────────────────────────\n'));
|
|
286
|
+
console.log(chalk.blue('💡 Open the dashboard to switch between projects.'));
|
|
287
|
+
return true;
|
|
288
|
+
} else {
|
|
289
|
+
throw new Error(response.error || 'Failed to register project');
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(chalk.red('❌ Failed to join leader:'), error.message);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Send HTTP request to server
|
|
299
|
+
* @param {number} port
|
|
300
|
+
* @param {string} path
|
|
301
|
+
* @param {Object} options
|
|
302
|
+
* @returns {Promise<Object>}
|
|
303
|
+
*/
|
|
304
|
+
sendHttpRequest(port, path, options = {}) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
const postData = options.body || '';
|
|
307
|
+
|
|
308
|
+
const req = http.request({
|
|
309
|
+
hostname: 'localhost',
|
|
310
|
+
port: port,
|
|
311
|
+
path: path,
|
|
312
|
+
method: options.method || 'GET',
|
|
313
|
+
headers: {
|
|
314
|
+
'Content-Type': 'application/json',
|
|
315
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
316
|
+
}
|
|
317
|
+
}, (res) => {
|
|
318
|
+
let data = '';
|
|
319
|
+
|
|
320
|
+
res.on('data', (chunk) => {
|
|
321
|
+
data += chunk;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
res.on('end', () => {
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(data);
|
|
327
|
+
resolve(parsed);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
reject(new Error('Invalid JSON response'));
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
req.on('error', (error) => {
|
|
335
|
+
reject(error);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
req.setTimeout(5000, () => {
|
|
339
|
+
req.destroy();
|
|
340
|
+
reject(new Error('Request timeout'));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (postData) {
|
|
344
|
+
req.write(postData);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
req.end();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Export singleton instance
|
|
353
|
+
module.exports = new ProjectManager();
|
|
@@ -5,7 +5,7 @@ const { stripAnsiCodes, classifyLogLine, extractErrors } = require(path.join(__d
|
|
|
5
5
|
|
|
6
6
|
class TerminalManager {
|
|
7
7
|
constructor() {
|
|
8
|
-
// Map: termId -> { process: pty, socketId: string }
|
|
8
|
+
// Map: termId -> { process: pty, socketId: string, projectId: string }
|
|
9
9
|
this.sessions = new Map();
|
|
10
10
|
// Map: termId -> Array of log lines (circular buffer, max 10000)
|
|
11
11
|
this.logBuffers = new Map();
|
|
@@ -22,7 +22,7 @@ class TerminalManager {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
createTerminal(socket, termId, cols = 80, rows = 24, cwd) {
|
|
25
|
+
createTerminal(socket, termId, cols = 80, rows = 24, cwd, projectId = null) {
|
|
26
26
|
try {
|
|
27
27
|
const term = pty.spawn(this.shell, [], {
|
|
28
28
|
name: 'xterm-256color',
|
|
@@ -34,7 +34,8 @@ class TerminalManager {
|
|
|
34
34
|
|
|
35
35
|
this.sessions.set(termId, {
|
|
36
36
|
process: term,
|
|
37
|
-
socketId: socket.id
|
|
37
|
+
socketId: socket.id,
|
|
38
|
+
projectId: projectId // Store project association
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
// Initialize log buffer for this terminal
|
|
@@ -185,6 +186,33 @@ class TerminalManager {
|
|
|
185
186
|
}))
|
|
186
187
|
};
|
|
187
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get all terminal IDs for a specific project
|
|
192
|
+
* @param {string} projectId - Project ID
|
|
193
|
+
* @returns {string[]} Array of terminal IDs
|
|
194
|
+
*/
|
|
195
|
+
getProjectTerminals(projectId) {
|
|
196
|
+
const terminals = [];
|
|
197
|
+
for (const [termId, session] of this.sessions.entries()) {
|
|
198
|
+
if (session.projectId === projectId) {
|
|
199
|
+
terminals.push(termId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return terminals;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clean up all terminals for a project
|
|
207
|
+
* @param {string} projectId - Project ID
|
|
208
|
+
*/
|
|
209
|
+
cleanupProject(projectId) {
|
|
210
|
+
const terminalsToRemove = this.getProjectTerminals(projectId);
|
|
211
|
+
terminalsToRemove.forEach(termId => {
|
|
212
|
+
this.kill(termId);
|
|
213
|
+
});
|
|
214
|
+
console.log(`Cleaned up ${terminalsToRemove.length} terminal(s) for project ${projectId}`);
|
|
215
|
+
}
|
|
188
216
|
}
|
|
189
217
|
|
|
190
218
|
module.exports = new TerminalManager();
|
|
@@ -94,44 +94,84 @@ body.resizing {
|
|
|
94
94
|
padding-right: 0px;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/* ---
|
|
97
|
+
/* --- HEADER REDESIGN --- */
|
|
98
98
|
.header {
|
|
99
99
|
display: flex;
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 6px; /* Space between rows */
|
|
102
102
|
margin-bottom: 15px;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
.header-
|
|
105
|
+
.header-top-row {
|
|
106
|
+
display: flex;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
align-items: center;
|
|
109
|
+
width: 100%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.header-left-group {
|
|
106
113
|
display: flex;
|
|
107
114
|
align-items: center;
|
|
108
115
|
gap: 12px;
|
|
116
|
+
flex: 1; /* Take available space */
|
|
117
|
+
min-width: 0; /* Enable truncation */
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
.status {
|
|
112
121
|
font-size: 14px;
|
|
113
122
|
color: var(--ios-green);
|
|
114
123
|
line-height: 1;
|
|
124
|
+
flex-shrink: 0;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
/*
|
|
118
|
-
.project-
|
|
127
|
+
/* Project Switcher replaces Project Title */
|
|
128
|
+
.project-switcher {
|
|
119
129
|
display: flex;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 8px;
|
|
132
|
+
flex: 1;
|
|
133
|
+
min-width: 0;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
.project-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
.project-selector {
|
|
137
|
+
padding: 5px 10px;
|
|
138
|
+
background: var(--ios-input-bg);
|
|
139
|
+
border: 1px solid var(--ios-separator);
|
|
140
|
+
border-radius: 6px;
|
|
141
|
+
font-size: 14px; /* Slightly bigger font like a title */
|
|
142
|
+
font-weight: 700; /* Bold like a title */
|
|
128
143
|
color: var(--text-primary);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
outline: none;
|
|
146
|
+
transition: all 0.2s;
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
text-overflow: ellipsis;
|
|
129
149
|
}
|
|
130
150
|
|
|
131
|
-
.project-
|
|
132
|
-
|
|
151
|
+
.project-selector:hover {
|
|
152
|
+
border-color: var(--ios-blue);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.project-selector:focus {
|
|
156
|
+
border-color: var(--ios-blue);
|
|
157
|
+
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.project-count {
|
|
161
|
+
font-size: 10px;
|
|
133
162
|
color: var(--text-secondary);
|
|
134
|
-
|
|
163
|
+
background: var(--ios-gray-light);
|
|
164
|
+
padding: 3px 7px;
|
|
165
|
+
border-radius: 10px;
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.header-actions {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
flex-shrink: 0;
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
.theme-toggle {
|
|
@@ -144,6 +184,45 @@ body.resizing {
|
|
|
144
184
|
box-shadow: 0 2px 5px var(--shadow-color);
|
|
145
185
|
}
|
|
146
186
|
|
|
187
|
+
.stop-server-btn {
|
|
188
|
+
width: 28px;
|
|
189
|
+
height: 28px;
|
|
190
|
+
border-radius: 50%;
|
|
191
|
+
border: none;
|
|
192
|
+
background: var(--ios-red);
|
|
193
|
+
color: white;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
box-shadow: 0 2px 5px var(--shadow-color);
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
transition: all 0.2s;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.stop-server-btn:hover {
|
|
204
|
+
opacity: 0.8;
|
|
205
|
+
transform: scale(1.05);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Bottom Row - Full Width Metadata */
|
|
209
|
+
.header-bottom-row {
|
|
210
|
+
width: 100%;
|
|
211
|
+
padding-left: 2px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.project-meta {
|
|
215
|
+
font-size: 11px;
|
|
216
|
+
color: var(--text-secondary);
|
|
217
|
+
font-family: monospace;
|
|
218
|
+
white-space: nowrap;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
text-overflow: ellipsis;
|
|
221
|
+
display: block;
|
|
222
|
+
width: 100%;
|
|
223
|
+
line-height: 1.4;
|
|
224
|
+
}
|
|
225
|
+
|
|
147
226
|
.system-prompt-card,
|
|
148
227
|
.endpoint-card {
|
|
149
228
|
background: var(--ios-card);
|
|
@@ -343,7 +422,7 @@ body.resizing {
|
|
|
343
422
|
flex: none;
|
|
344
423
|
width: 100%;
|
|
345
424
|
max-width: 100%;
|
|
346
|
-
height:
|
|
425
|
+
height: 100vh;
|
|
347
426
|
border-right: none;
|
|
348
427
|
border-bottom: 1px solid var(--ios-separator);
|
|
349
428
|
}
|
|
@@ -352,6 +431,7 @@ body.resizing {
|
|
|
352
431
|
/* --- SAVED COMMANDS STYLES --- */
|
|
353
432
|
.saved-commands-panel {
|
|
354
433
|
margin-bottom: 12px;
|
|
434
|
+
background: var(--ios-card);
|
|
355
435
|
}
|
|
356
436
|
|
|
357
437
|
.saved-commands-header {
|
|
@@ -423,71 +503,97 @@ body.resizing {
|
|
|
423
503
|
margin-top: 0;
|
|
424
504
|
}
|
|
425
505
|
|
|
506
|
+
/* NEW: Grid Layout for Commands */
|
|
426
507
|
.commands-list {
|
|
427
|
-
display:
|
|
428
|
-
|
|
508
|
+
display: grid;
|
|
509
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
429
510
|
gap: 8px;
|
|
511
|
+
max-height: 300px;
|
|
512
|
+
overflow-y: auto;
|
|
513
|
+
padding-right: 2px; /* For scrollbar */
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* Custom Scrollbar for list */
|
|
517
|
+
.commands-list::-webkit-scrollbar {
|
|
518
|
+
width: 4px;
|
|
519
|
+
}
|
|
520
|
+
.commands-list::-webkit-scrollbar-thumb {
|
|
521
|
+
background: var(--ios-separator);
|
|
522
|
+
border-radius: 2px;
|
|
430
523
|
}
|
|
431
524
|
|
|
432
525
|
.command-card {
|
|
433
526
|
display: flex;
|
|
434
527
|
justify-content: space-between;
|
|
435
528
|
align-items: center;
|
|
436
|
-
padding: 8px
|
|
529
|
+
padding: 6px 8px; /* Tighter padding */
|
|
437
530
|
background: var(--ios-input-bg);
|
|
438
531
|
border-radius: 6px;
|
|
439
532
|
cursor: pointer;
|
|
440
533
|
transition: all 0.2s;
|
|
441
534
|
border: 1px solid transparent;
|
|
535
|
+
height: 42px; /* Fixed height for consistency */
|
|
536
|
+
overflow: hidden;
|
|
442
537
|
}
|
|
443
538
|
|
|
444
539
|
.command-card:hover {
|
|
445
540
|
background: var(--ios-gray-light);
|
|
446
541
|
border-color: var(--ios-blue);
|
|
447
|
-
transform:
|
|
542
|
+
transform: translateY(-1px);
|
|
448
543
|
}
|
|
449
544
|
|
|
450
545
|
.command-card-main {
|
|
451
546
|
display: flex;
|
|
452
547
|
align-items: center;
|
|
453
|
-
gap:
|
|
548
|
+
gap: 8px;
|
|
454
549
|
flex: 1;
|
|
455
|
-
min-width: 0;
|
|
550
|
+
min-width: 0; /* Critical for truncation */
|
|
456
551
|
}
|
|
457
552
|
|
|
458
553
|
.command-icon {
|
|
459
|
-
font-size:
|
|
554
|
+
font-size: 16px;
|
|
460
555
|
flex-shrink: 0;
|
|
556
|
+
width: 20px;
|
|
557
|
+
text-align: center;
|
|
461
558
|
}
|
|
462
559
|
|
|
463
560
|
.command-info {
|
|
464
561
|
display: flex;
|
|
465
562
|
flex-direction: column;
|
|
466
|
-
gap:
|
|
563
|
+
gap: 1px; /* Tighter gap */
|
|
467
564
|
min-width: 0;
|
|
468
565
|
flex: 1;
|
|
469
566
|
}
|
|
470
567
|
|
|
471
568
|
.command-name {
|
|
472
569
|
font-weight: 600;
|
|
473
|
-
font-size:
|
|
570
|
+
font-size: 12px;
|
|
474
571
|
color: var(--text-primary);
|
|
572
|
+
white-space: nowrap;
|
|
573
|
+
overflow: hidden;
|
|
574
|
+
text-overflow: ellipsis;
|
|
575
|
+
line-height: 1.2;
|
|
475
576
|
}
|
|
476
577
|
|
|
477
578
|
.command-text {
|
|
478
579
|
font-family: monospace;
|
|
479
|
-
font-size:
|
|
580
|
+
font-size: 10px;
|
|
480
581
|
color: var(--text-secondary);
|
|
481
582
|
white-space: nowrap;
|
|
482
583
|
overflow: hidden;
|
|
483
584
|
text-overflow: ellipsis;
|
|
585
|
+
line-height: 1.2;
|
|
484
586
|
}
|
|
485
587
|
|
|
486
588
|
.command-card-actions {
|
|
487
589
|
display: flex;
|
|
488
|
-
gap:
|
|
590
|
+
gap: 2px;
|
|
489
591
|
opacity: 0;
|
|
490
592
|
transition: opacity 0.2s;
|
|
593
|
+
margin-left: 4px;
|
|
594
|
+
flex-shrink: 0;
|
|
595
|
+
background: var(--ios-gray-light); /* Ensure visibility over background */
|
|
596
|
+
border-radius: 4px;
|
|
491
597
|
}
|
|
492
598
|
|
|
493
599
|
.command-card:hover .command-card-actions {
|
|
@@ -495,21 +601,23 @@ body.resizing {
|
|
|
495
601
|
}
|
|
496
602
|
|
|
497
603
|
.command-action-btn {
|
|
498
|
-
width:
|
|
499
|
-
height:
|
|
604
|
+
width: 22px;
|
|
605
|
+
height: 22px;
|
|
500
606
|
border: none;
|
|
501
|
-
background:
|
|
607
|
+
background: transparent;
|
|
502
608
|
border-radius: 4px;
|
|
503
609
|
cursor: pointer;
|
|
504
610
|
display: flex;
|
|
505
611
|
align-items: center;
|
|
506
612
|
justify-content: center;
|
|
507
613
|
font-size: 12px;
|
|
614
|
+
color: var(--text-secondary);
|
|
508
615
|
transition: all 0.2s;
|
|
509
616
|
}
|
|
510
617
|
|
|
511
618
|
.command-action-btn:hover {
|
|
512
619
|
background: rgba(0, 0, 0, 0.1);
|
|
620
|
+
color: var(--text-primary);
|
|
513
621
|
transform: scale(1.1);
|
|
514
622
|
}
|
|
515
623
|
|