antri_cli 1.27.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import https from 'https';
5
+ import { profileManager } from '../profiles/profileManager.js';
6
+ export class FirestoreSyncManager {
7
+ static getSyncConfigFile() {
8
+ return path.join(os.homedir(), '.antri', 'cloud_sync.json');
9
+ }
10
+ static getSyncConfig() {
11
+ const filePath = this.getSyncConfigFile();
12
+ if (fs.existsSync(filePath)) {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
15
+ }
16
+ catch (_) { }
17
+ }
18
+ return {
19
+ projectId: process.env.GOOGLE_CLOUD_PROJECT || process.env.GCP_PROJECT || '',
20
+ syncKey: 'default_user',
21
+ apiKey: process.env.GEMINI_API_KEY || '',
22
+ };
23
+ }
24
+ static saveSyncConfig(projectId, syncKey = 'default_user', apiKey = '') {
25
+ const dir = path.join(os.homedir(), '.antri');
26
+ if (!fs.existsSync(dir))
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ fs.writeFileSync(this.getSyncConfigFile(), JSON.stringify({ projectId, syncKey, apiKey, lastSynced: new Date().toISOString() }, null, 2), 'utf-8');
29
+ }
30
+ /**
31
+ * Push local profiles from ~/.antri/profiles/ to Google Cloud Firestore
32
+ */
33
+ static async pushToFirestore() {
34
+ const { projectId, syncKey, apiKey } = this.getSyncConfig();
35
+ if (!projectId) {
36
+ return { success: false, count: 0, error: 'Google Cloud Project ID is not configured. Run "antri sync config <project-id>"' };
37
+ }
38
+ const profiles = profileManager.listProfiles();
39
+ let synced = 0;
40
+ const keyParam = apiKey ? `?key=${apiKey}` : '';
41
+ for (const p of profiles) {
42
+ const filePath = path.join(os.homedir(), '.antri', 'profiles', `${p.name}.md`);
43
+ let content = '';
44
+ if (fs.existsSync(filePath)) {
45
+ content = fs.readFileSync(filePath, 'utf-8');
46
+ }
47
+ const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/antri_sync/${syncKey}/profiles/${p.name}${keyParam}`;
48
+ const payload = JSON.stringify({
49
+ fields: {
50
+ name: { stringValue: p.name },
51
+ content: { stringValue: content },
52
+ updatedAt: { stringValue: new Date().toISOString() },
53
+ },
54
+ });
55
+ try {
56
+ await this.httpRequest(url, 'PATCH', payload);
57
+ synced++;
58
+ }
59
+ catch (err) {
60
+ // Continue attempting others
61
+ }
62
+ }
63
+ this.saveSyncConfig(projectId, syncKey, apiKey);
64
+ return { success: true, count: synced };
65
+ }
66
+ /**
67
+ * Pull profiles from Google Cloud Firestore to ~/.antri/profiles/
68
+ */
69
+ static async pullFromFirestore() {
70
+ const { projectId, syncKey } = this.getSyncConfig();
71
+ if (!projectId) {
72
+ return { success: false, count: 0, error: 'Google Cloud Project ID is not configured.' };
73
+ }
74
+ const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/antri_sync/${syncKey}/profiles`;
75
+ try {
76
+ const raw = await this.httpRequest(url, 'GET');
77
+ const data = JSON.parse(raw);
78
+ const docs = data.documents || [];
79
+ const dir = path.join(os.homedir(), '.antri', 'profiles');
80
+ if (!fs.existsSync(dir))
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ let count = 0;
83
+ for (const doc of docs) {
84
+ const fields = doc.fields || {};
85
+ const name = fields.name?.stringValue;
86
+ const content = fields.content?.stringValue;
87
+ if (name && content) {
88
+ fs.writeFileSync(path.join(dir, `${name}.md`), content, 'utf-8');
89
+ count++;
90
+ }
91
+ }
92
+ this.saveSyncConfig(projectId, syncKey);
93
+ return { success: true, count };
94
+ }
95
+ catch (err) {
96
+ return { success: false, count: 0, error: err.message };
97
+ }
98
+ }
99
+ static httpRequest(urlStr, method, body) {
100
+ return new Promise((resolve, reject) => {
101
+ const url = new URL(urlStr);
102
+ const options = {
103
+ hostname: url.hostname,
104
+ port: 443,
105
+ path: url.pathname + url.search,
106
+ method,
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ 'Content-Length': body ? Buffer.byteLength(body) : 0,
110
+ },
111
+ };
112
+ const req = https.request(options, (res) => {
113
+ let resBody = '';
114
+ res.on('data', (d) => (resBody += d));
115
+ res.on('end', () => {
116
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
117
+ resolve(resBody);
118
+ }
119
+ else {
120
+ reject(new Error(`HTTP ${res.statusCode}: ${resBody}`));
121
+ }
122
+ });
123
+ });
124
+ req.on('error', (e) => reject(e));
125
+ if (body)
126
+ req.write(body);
127
+ req.end();
128
+ });
129
+ }
130
+ }
131
+ //# sourceMappingURL=firestore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore.js","sourceRoot":"","sources":["../../src/cloud/firestore.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
5
5
  // Load .env if present
6
6
  dotenv.config();
7
7
  const DEFAULT_CONFIG = {
8
- version: '1.27.0',
8
+ version: '1.29.0',
9
9
  provider: 'deepseek',
10
10
  model: 'deepseek-v4-flash-(latest)',
11
11
  mode: 'vibe',
@@ -1,5 +1,5 @@
1
1
  export declare class Updater {
2
- static readonly CURRENT_VERSION = "1.27.0";
2
+ static readonly CURRENT_VERSION = "1.29.0";
3
3
  static readonly PACKAGE_NAME = "antri_cli";
4
4
  /**
5
5
  * Checks for available updates and self-updates the global antri CLI
@@ -4,7 +4,7 @@ import chalk from 'chalk';
4
4
  import ora from 'ora';
5
5
  const execPromise = util.promisify(exec);
6
6
  export class Updater {
7
- static CURRENT_VERSION = '1.27.0';
7
+ static CURRENT_VERSION = '1.29.0';
8
8
  static PACKAGE_NAME = 'antri_cli';
9
9
  /**
10
10
  * Checks for available updates and self-updates the global antri CLI
@@ -2,10 +2,16 @@
2
2
 
3
3
  let currentConfig = null;
4
4
  let activeTab = 'chat';
5
+ let attachedFiles = [];
6
+ let availableCommands = [];
7
+ let activePaletteMatches = [];
8
+ let paletteSelectedIndex = 0;
9
+ let activePaletteMode = null; // 'slash' | 'file' | null
5
10
 
6
11
  // Initialize on Load
7
12
  document.addEventListener('DOMContentLoaded', async () => {
8
13
  await loadStatus();
14
+ await loadCommands();
9
15
  await loadProfiles();
10
16
  await loadSkills();
11
17
  await loadMemory();
@@ -34,6 +40,17 @@ async function loadStatus() {
34
40
  }
35
41
  }
36
42
 
43
+ // Load Slash Commands for Prompt Toolkit
44
+ async function loadCommands() {
45
+ try {
46
+ const res = await fetch('/api/commands');
47
+ const data = await res.json();
48
+ availableCommands = data.commands || [];
49
+ } catch (err) {
50
+ console.error('Failed to load commands:', err);
51
+ }
52
+ }
53
+
37
54
  // Load Models for Provider
38
55
  async function loadModels() {
39
56
  try {
@@ -132,13 +149,243 @@ function showTab(tabName) {
132
149
  if (navButtons[navIndex]) navButtons[navIndex].classList.add('active');
133
150
  }
134
151
 
152
+ // File Upload Handler (Images & Files)
153
+ async function handleFileSelected(event) {
154
+ const files = event.target.files;
155
+ if (!files || files.length === 0) return;
156
+
157
+ for (const file of files) {
158
+ const reader = new FileReader();
159
+ reader.onload = async (e) => {
160
+ const data = e.target.result;
161
+ try {
162
+ const res = await fetch('/api/upload', {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({
166
+ fileName: file.name,
167
+ fileType: file.type || 'text/plain',
168
+ data,
169
+ }),
170
+ });
171
+ const uploadRes = await res.json();
172
+ if (uploadRes.success) {
173
+ attachedFiles.push({
174
+ name: file.name,
175
+ path: uploadRes.filePath,
176
+ isImage: uploadRes.isImage,
177
+ dataUrl: uploadRes.isImage ? data : null,
178
+ });
179
+ renderAttachmentChips();
180
+ }
181
+ } catch (err) {
182
+ console.error('File upload failed:', err);
183
+ }
184
+ };
185
+ if (file.type.startsWith('image/')) {
186
+ reader.readAsDataURL(file);
187
+ } else {
188
+ reader.readAsText(file);
189
+ }
190
+ }
191
+
192
+ // Reset input
193
+ event.target.value = '';
194
+ }
195
+
196
+ function renderAttachmentChips() {
197
+ const tray = document.getElementById('attachment-preview-tray');
198
+ if (!tray) return;
199
+
200
+ if (attachedFiles.length === 0) {
201
+ tray.classList.add('hidden');
202
+ tray.innerHTML = '';
203
+ return;
204
+ }
205
+
206
+ tray.classList.remove('hidden');
207
+ tray.innerHTML = '';
208
+
209
+ attachedFiles.forEach((file, index) => {
210
+ const chip = document.createElement('div');
211
+ chip.className = 'attachment-chip';
212
+ if (file.isImage && file.dataUrl) {
213
+ chip.innerHTML = `
214
+ <img src="${file.dataUrl}" alt="preview" />
215
+ <span>${file.name}</span>
216
+ <button class="remove-chip-btn" onclick="removeAttachment(${index})">×</button>
217
+ `;
218
+ } else {
219
+ chip.innerHTML = `
220
+ <span>${file.name}</span>
221
+ <button class="remove-chip-btn" onclick="removeAttachment(${index})">×</button>
222
+ `;
223
+ }
224
+ tray.appendChild(chip);
225
+ });
226
+ }
227
+
228
+ function removeAttachment(index) {
229
+ attachedFiles.splice(index, 1);
230
+ renderAttachmentChips();
231
+ }
232
+
233
+ // Prompt Toolkit Text & Key Handler
234
+ async function handleInputText(event) {
235
+ const val = event.target.value;
236
+ const cursorPos = event.target.selectionStart;
237
+
238
+ // 1. Slash command mode
239
+ if (val.startsWith('/')) {
240
+ const query = val.toLowerCase();
241
+ activePaletteMatches = availableCommands.filter((cmd) => {
242
+ const baseName = cmd.name.split(' ')[0].toLowerCase();
243
+ return baseName.startsWith(query) || cmd.name.toLowerCase().startsWith(query);
244
+ });
245
+ activePaletteMode = 'slash';
246
+ renderPalette('Commands', activePaletteMatches);
247
+ return;
248
+ }
249
+
250
+ // 2. Attachment file mode (@)
251
+ const lastAt = val.lastIndexOf('@', cursorPos - 1);
252
+ if (lastAt !== -1 && (lastAt === 0 || val[lastAt - 1] === ' ')) {
253
+ const query = val.slice(lastAt + 1, cursorPos);
254
+ if (!query.includes(' ')) {
255
+ try {
256
+ const res = await fetch(`/api/files?query=${encodeURIComponent(query)}`);
257
+ const data = await res.json();
258
+ activePaletteMatches = data.items.map((item) => ({
259
+ name: item.name,
260
+ description: item.isDirectory ? 'Directory' : item.relativePath,
261
+ relativePath: item.relativePath,
262
+ isDirectory: item.isDirectory,
263
+ }));
264
+ activePaletteMode = 'file';
265
+ renderPalette(`Files: ${data.currentDir}`, activePaletteMatches);
266
+ return;
267
+ } catch (e) {}
268
+ }
269
+ }
270
+
271
+ hidePalette();
272
+ }
273
+
274
+ function renderPalette(title, items) {
275
+ const palette = document.getElementById('prompt-toolkit-palette');
276
+ const header = document.getElementById('palette-header');
277
+ const list = document.getElementById('palette-list');
278
+
279
+ if (!palette || !items || items.length === 0) {
280
+ hidePalette();
281
+ return;
282
+ }
283
+
284
+ header.textContent = title;
285
+ list.innerHTML = '';
286
+ paletteSelectedIndex = Math.min(paletteSelectedIndex, items.length - 1);
287
+ if (paletteSelectedIndex < 0) paletteSelectedIndex = 0;
288
+
289
+ items.slice(0, 10).forEach((item, idx) => {
290
+ const el = document.createElement('div');
291
+ el.className = `palette-item ${idx === paletteSelectedIndex ? 'active' : ''}`;
292
+ el.innerHTML = `
293
+ <span class="palette-name">${item.name}</span>
294
+ <span class="palette-desc">${item.description || ''}</span>
295
+ ${item.isDirectory ? '<span class="palette-tag">dir</span>' : ''}
296
+ `;
297
+ el.onclick = () => selectPaletteItem(idx);
298
+ list.appendChild(el);
299
+ });
300
+
301
+ palette.classList.remove('hidden');
302
+ }
303
+
304
+ function hidePalette() {
305
+ const palette = document.getElementById('prompt-toolkit-palette');
306
+ if (palette) palette.classList.add('hidden');
307
+ activePaletteMode = null;
308
+ activePaletteMatches = [];
309
+ paletteSelectedIndex = 0;
310
+ }
311
+
312
+ function selectPaletteItem(index) {
313
+ const item = activePaletteMatches[index];
314
+ if (!item) return;
315
+
316
+ const input = document.getElementById('prompt-input');
317
+
318
+ if (activePaletteMode === 'slash') {
319
+ const rawCmd = item.name.split(' ')[0];
320
+ input.value = rawCmd + ' ';
321
+ hidePalette();
322
+ input.focus();
323
+ } else if (activePaletteMode === 'file') {
324
+ const val = input.value;
325
+ const cursorPos = input.selectionStart;
326
+ const lastAt = val.lastIndexOf('@', cursorPos - 1);
327
+ if (lastAt !== -1) {
328
+ input.value = val.slice(0, lastAt) + '@' + item.relativePath + ' ' + val.slice(cursorPos);
329
+ }
330
+ hidePalette();
331
+ input.focus();
332
+ }
333
+ }
334
+
335
+ function handleInputKey(event) {
336
+ const palette = document.getElementById('prompt-toolkit-palette');
337
+ const isPaletteVisible = palette && !palette.classList.contains('hidden');
338
+
339
+ if (isPaletteVisible && activePaletteMatches.length > 0) {
340
+ if (event.key === 'ArrowUp') {
341
+ event.preventDefault();
342
+ paletteSelectedIndex = (paletteSelectedIndex - 1 + activePaletteMatches.length) % activePaletteMatches.length;
343
+ renderPalette(document.getElementById('palette-header').textContent, activePaletteMatches);
344
+ return;
345
+ }
346
+ if (event.key === 'ArrowDown') {
347
+ event.preventDefault();
348
+ paletteSelectedIndex = (paletteSelectedIndex + 1) % activePaletteMatches.length;
349
+ renderPalette(document.getElementById('palette-header').textContent, activePaletteMatches);
350
+ return;
351
+ }
352
+ if (event.key === 'Tab' || event.key === 'Enter') {
353
+ if (!event.ctrlKey && !event.metaKey) {
354
+ event.preventDefault();
355
+ selectPaletteItem(paletteSelectedIndex);
356
+ return;
357
+ }
358
+ }
359
+ if (event.key === 'Escape') {
360
+ hidePalette();
361
+ return;
362
+ }
363
+ }
364
+
365
+ // Ctrl + Enter to submit prompt
366
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
367
+ event.preventDefault();
368
+ submitPrompt();
369
+ }
370
+ }
371
+
135
372
  // Chat Prompt Submission with SSE Streaming
136
373
  async function submitPrompt() {
137
374
  const input = document.getElementById('prompt-input');
138
- const prompt = input.value.trim();
375
+ let prompt = input.value.trim();
376
+
377
+ // Attach any uploaded files to prompt
378
+ if (attachedFiles.length > 0) {
379
+ const attachmentsText = attachedFiles.map((f) => `\n[Attached File: ${f.name} (${f.path})]`).join('');
380
+ prompt = prompt + '\n' + attachmentsText;
381
+ attachedFiles = [];
382
+ renderAttachmentChips();
383
+ }
384
+
139
385
  if (!prompt) return;
140
386
 
141
387
  input.value = '';
388
+ hidePalette();
142
389
 
143
390
  // Intercept /debate or /goal inside chat
144
391
  if (prompt.startsWith('/debate')) {
@@ -232,13 +479,6 @@ function setPrompt(text) {
232
479
  input.focus();
233
480
  }
234
481
 
235
- function handleInputKey(event) {
236
- if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
237
- event.preventDefault();
238
- submitPrompt();
239
- }
240
- }
241
-
242
482
  // Dialectic Debate Runner
243
483
  async function startDebate() {
244
484
  const input = document.getElementById('debate-query-input');
@@ -15,7 +15,7 @@
15
15
  <header class="app-header">
16
16
  <div class="brand-zone">
17
17
  <span class="logo-mark">ANTRI</span>
18
- <span class="version-tag">v1.27.0</span>
18
+ <span class="version-tag">v1.29.0</span>
19
19
  </div>
20
20
 
21
21
  <!-- Mode Toggle -->
@@ -116,12 +116,32 @@
116
116
  </div>
117
117
  </div>
118
118
 
119
- <!-- Bottom Prompt Box -->
119
+ <!-- Bottom Prompt Box & Prompt Toolkit -->
120
120
  <div class="chat-input-bar">
121
+ <!-- Prompt Toolkit Dropdown Palette -->
122
+ <div id="prompt-toolkit-palette" class="prompt-toolkit-palette hidden">
123
+ <div class="palette-header" id="palette-header">Commands</div>
124
+ <div class="palette-list" id="palette-list">
125
+ <!-- Dynamically populated -->
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Attached Files Preview Tray -->
130
+ <div id="attachment-preview-tray" class="attachment-preview-tray hidden"></div>
131
+
121
132
  <div class="input-container">
122
- <textarea id="prompt-input" placeholder="Ask ANTRI, type /plan, /goal, or @ to attach files..." rows="2" onkeydown="handleInputKey(event)"></textarea>
133
+ <!-- Hidden File Input -->
134
+ <input type="file" id="file-upload-input" multiple accept="image/*,.txt,.ts,.js,.json,.py,.md,.csv,.html,.css" style="display:none;" onchange="handleFileSelected(event)" />
135
+
136
+ <div class="input-row">
137
+ <button id="btn-attach-file" class="attach-btn" title="Upload image or file" onclick="document.getElementById('file-upload-input').click()">
138
+ +
139
+ </button>
140
+ <textarea id="prompt-input" placeholder="Ask ANTRI, type / for commands, @ for files..." rows="2" onkeydown="handleInputKey(event)" oninput="handleInputText(event)"></textarea>
141
+ </div>
142
+
123
143
  <div class="input-actions">
124
- <span class="shortcut-tip">Press Ctrl + Enter to send · @ for files</span>
144
+ <span class="shortcut-tip">Press <b>Ctrl + Enter</b> to send · <b>/</b> commands · <b>@</b> files</span>
125
145
  <button id="send-btn" class="send-btn" onclick="submitPrompt()">
126
146
  <span>Send</span>
127
147
  </button>
@@ -394,25 +394,175 @@ body.antri-app {
394
394
  display: inline-block;
395
395
  }
396
396
 
397
- /* Chat Input Bar */
397
+ /* Chat Input Bar & Attachments */
398
398
  .chat-input-bar {
399
399
  position: absolute;
400
400
  bottom: 1.25rem;
401
401
  left: 2.75rem;
402
402
  right: 2.75rem;
403
+ z-index: 50;
403
404
  }
404
405
 
405
406
  .input-container {
406
407
  background: var(--bg-card);
407
408
  border: 1px solid var(--border-main);
408
409
  border-radius: var(--radius-lg);
409
- padding: 1.1rem 1.35rem;
410
+ padding: 0.9rem 1.25rem;
410
411
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.04);
411
412
  display: flex;
412
413
  flex-direction: column;
414
+ gap: 0.5rem;
415
+ position: relative;
416
+ }
417
+
418
+ .input-row {
419
+ display: flex;
420
+ align-items: flex-start;
413
421
  gap: 0.75rem;
414
422
  }
415
423
 
424
+ .attach-btn {
425
+ width: 32px;
426
+ height: 32px;
427
+ min-width: 32px;
428
+ border-radius: var(--radius-sm);
429
+ border: 1px solid var(--border-main);
430
+ background: var(--bg-subtle);
431
+ color: var(--text-primary);
432
+ font-size: 1.15rem;
433
+ font-weight: 500;
434
+ display: flex;
435
+ align-items: center;
436
+ justify-content: center;
437
+ cursor: pointer;
438
+ transition: all 0.15s ease;
439
+ margin-top: 2px;
440
+ }
441
+
442
+ .attach-btn:hover {
443
+ background: var(--bg-hover);
444
+ border-color: var(--text-primary);
445
+ }
446
+
447
+ .attachment-preview-tray {
448
+ display: flex;
449
+ flex-wrap: wrap;
450
+ gap: 0.5rem;
451
+ padding: 0.4rem 0.2rem;
452
+ border-bottom: 1px solid var(--border-light);
453
+ margin-bottom: 0.25rem;
454
+ }
455
+
456
+ .attachment-chip {
457
+ display: flex;
458
+ align-items: center;
459
+ gap: 0.4rem;
460
+ background: var(--bg-subtle);
461
+ border: 1px solid var(--border-main);
462
+ padding: 0.25rem 0.6rem;
463
+ border-radius: var(--radius-xs);
464
+ font-size: 0.75rem;
465
+ font-family: var(--font-mono);
466
+ color: var(--text-primary);
467
+ }
468
+
469
+ .attachment-chip img {
470
+ width: 18px;
471
+ height: 18px;
472
+ object-fit: cover;
473
+ border-radius: 2px;
474
+ }
475
+
476
+ .attachment-chip .remove-chip-btn {
477
+ background: transparent;
478
+ border: none;
479
+ color: var(--text-tertiary);
480
+ font-weight: 700;
481
+ cursor: pointer;
482
+ padding: 0 0.2rem;
483
+ }
484
+
485
+ .attachment-chip .remove-chip-btn:hover {
486
+ color: var(--accent-warning);
487
+ }
488
+
489
+ /* Prompt Toolkit Autocomplete Palette */
490
+ .prompt-toolkit-palette {
491
+ position: absolute;
492
+ bottom: calc(100% + 0.5rem);
493
+ left: 0;
494
+ right: 0;
495
+ background: var(--bg-card);
496
+ border: 1px solid var(--border-main);
497
+ border-radius: var(--radius-md);
498
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
499
+ max-height: 280px;
500
+ overflow-y: auto;
501
+ display: flex;
502
+ flex-direction: column;
503
+ z-index: 100;
504
+ }
505
+
506
+ .palette-header {
507
+ padding: 0.5rem 0.9rem;
508
+ background: var(--bg-sidebar);
509
+ border-bottom: 1px solid var(--border-main);
510
+ font-size: 0.7rem;
511
+ text-transform: uppercase;
512
+ letter-spacing: 0.05em;
513
+ font-weight: 700;
514
+ color: var(--text-tertiary);
515
+ }
516
+
517
+ .palette-list {
518
+ display: flex;
519
+ flex-direction: column;
520
+ }
521
+
522
+ .palette-item {
523
+ display: flex;
524
+ align-items: center;
525
+ justify-content: space-between;
526
+ padding: 0.6rem 1rem;
527
+ cursor: pointer;
528
+ border-bottom: 1px solid var(--border-light);
529
+ transition: all 0.1s ease;
530
+ }
531
+
532
+ .palette-item:last-child {
533
+ border-bottom: none;
534
+ }
535
+
536
+ .palette-item.active,
537
+ .palette-item:hover {
538
+ background: var(--bg-subtle);
539
+ }
540
+
541
+ .palette-name {
542
+ font-family: var(--font-mono);
543
+ font-size: 0.84rem;
544
+ font-weight: 600;
545
+ color: var(--text-primary);
546
+ min-width: 140px;
547
+ }
548
+
549
+ .palette-desc {
550
+ font-size: 0.78rem;
551
+ color: var(--text-secondary);
552
+ flex: 1;
553
+ margin-left: 1rem;
554
+ }
555
+
556
+ .palette-tag {
557
+ font-size: 0.7rem;
558
+ color: var(--text-tertiary);
559
+ font-family: var(--font-mono);
560
+ }
561
+
562
+ .hidden {
563
+ display: none !important;
564
+ }
565
+
416
566
  .input-container textarea {
417
567
  background: transparent;
418
568
  border: none;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/desktop/server.ts"],"names":[],"mappings":"AAiCA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,WAAW,CAAa;;IAMnB,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkExB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAUpB,SAAS;WAyMH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CA0BnD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/desktop/server.ts"],"names":[],"mappings":"AAoCA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,WAAW,CAAa;;IAMnB,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAqExB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAUpB,SAAS;WA8QH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBnD"}