antri_cli 1.27.0 → 1.28.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.
@@ -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.28.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;WAyPH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBnD"}
@@ -1,6 +1,7 @@
1
1
  import http from 'http';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import os from 'os';
4
5
  import { fileURLToPath } from 'url';
5
6
  import { spawn } from 'child_process';
6
7
  import { configManager } from '../core/config.js';
@@ -12,6 +13,8 @@ import { memoryManager } from '../memory/manager.js';
12
13
  import { SkillSynthesizer } from '../core/skillSynthesizer.js';
13
14
  import { getAllActiveTools } from '../core/tools.js';
14
15
  import { getAvailableModels } from '../providers/models.js';
16
+ import { PROMPT_TOOLKIT_COMMANDS } from '../cli/promptToolkit.js';
17
+ import { FilePickerService } from '../cli/dialogs/filePicker.js';
15
18
  import chalk from 'chalk';
16
19
  const __filename = fileURLToPath(import.meta.url);
17
20
  const __dirname = path.dirname(__filename);
@@ -52,7 +55,7 @@ export class DesktopServer {
52
55
  const pathname = url.pathname;
53
56
  // 1. API Endpoints
54
57
  if (pathname.startsWith('/api/')) {
55
- await this.handleApi(pathname, req, res);
58
+ await this.handleApi(pathname, url, req, res);
56
59
  return;
57
60
  }
58
61
  // 2. Static File Serving
@@ -68,6 +71,9 @@ export class DesktopServer {
68
71
  '.json': 'application/json',
69
72
  '.png': 'image/png',
70
73
  '.svg': 'image/svg+xml',
74
+ '.jpg': 'image/jpeg',
75
+ '.jpeg': 'image/jpeg',
76
+ '.webp': 'image/webp',
71
77
  };
72
78
  try {
73
79
  const data = fs.readFileSync(filePath);
@@ -103,7 +109,7 @@ export class DesktopServer {
103
109
  }
104
110
  });
105
111
  }
106
- async handleApi(pathname, req, res) {
112
+ async handleApi(pathname, url, req, res) {
107
113
  const config = configManager.get();
108
114
  // GET /api/status
109
115
  if (pathname === '/api/status' && req.method === 'GET') {
@@ -115,6 +121,20 @@ export class DesktopServer {
115
121
  }));
116
122
  return;
117
123
  }
124
+ // GET /api/commands (Prompt Toolkit Slash Commands)
125
+ if (pathname === '/api/commands' && req.method === 'GET') {
126
+ res.writeHead(200, { 'Content-Type': 'application/json' });
127
+ res.end(JSON.stringify({ commands: PROMPT_TOOLKIT_COMMANDS }));
128
+ return;
129
+ }
130
+ // GET /api/files (Prompt Toolkit File Browser for @)
131
+ if (pathname === '/api/files' && req.method === 'GET') {
132
+ const query = url.searchParams.get('query') || '';
133
+ const { currentDir, items } = FilePickerService.listDirectory(config.workingDir, query);
134
+ res.writeHead(200, { 'Content-Type': 'application/json' });
135
+ res.end(JSON.stringify({ currentDir, items }));
136
+ return;
137
+ }
118
138
  // GET /api/models
119
139
  if (pathname === '/api/models' && req.method === 'GET') {
120
140
  const models = await getAvailableModels(config);
@@ -153,6 +173,33 @@ export class DesktopServer {
153
173
  req.on('end', async () => {
154
174
  try {
155
175
  const payload = body ? JSON.parse(body) : {};
176
+ // POST /api/upload (File and Image Uploader)
177
+ if (pathname === '/api/upload' && req.method === 'POST') {
178
+ const fileName = payload.fileName || `upload_${Date.now()}.bin`;
179
+ const fileData = payload.data || ''; // Base64 or text data
180
+ const fileType = payload.fileType || 'text/plain';
181
+ const uploadDir = path.join(os.homedir(), '.antri', 'attachments');
182
+ if (!fs.existsSync(uploadDir)) {
183
+ fs.mkdirSync(uploadDir, { recursive: true });
184
+ }
185
+ const savedPath = path.join(uploadDir, fileName);
186
+ if (fileData.startsWith('data:')) {
187
+ const base64Data = fileData.split(',')[1];
188
+ fs.writeFileSync(savedPath, Buffer.from(base64Data, 'base64'));
189
+ }
190
+ else {
191
+ fs.writeFileSync(savedPath, fileData, 'utf-8');
192
+ }
193
+ res.writeHead(200, { 'Content-Type': 'application/json' });
194
+ res.end(JSON.stringify({
195
+ success: true,
196
+ filePath: savedPath,
197
+ fileName,
198
+ fileType,
199
+ isImage: fileType.startsWith('image/'),
200
+ }));
201
+ return;
202
+ }
156
203
  // POST /api/chat (SSE Streaming)
157
204
  if (pathname === '/api/chat' && req.method === 'POST') {
158
205
  res.writeHead(200, {
@@ -262,7 +309,7 @@ export class DesktopServer {
262
309
  // POST /api/profile/save
263
310
  if (pathname === '/api/profile/save' && req.method === 'POST') {
264
311
  const active = profileManager.getActiveProfileName();
265
- const filePath = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.antri', 'profiles', `${active}.md`);
312
+ const filePath = path.join(os.homedir(), '.antri', 'profiles', `${active}.md`);
266
313
  fs.writeFileSync(filePath, payload.content, 'utf-8');
267
314
  res.writeHead(200, { 'Content-Type': 'application/json' });
268
315
  res.end(JSON.stringify({ success: true }));
@@ -289,7 +336,7 @@ export class DesktopServer {
289
336
  const port = await desktop.start();
290
337
  const url = `http://localhost:${port}`;
291
338
  console.log();
292
- console.log(chalk.bgHex('#7c3aed').bold.white(' ANTRI DESKTOP APP LAUNCHED '));
339
+ console.log(chalk.bgHex('#7c3aed').bold.white(' ANTRI DESKTOP APP LAUNCHED '));
293
340
  console.log(chalk.hex('#a5b4fc')(`Local Desktop Control Plane running at: ${chalk.bold.cyan(url)}`));
294
341
  console.log(chalk.hex('#64748b')('Shared Memory, Thinking Profiles, Skills & Providers actively synced with CLI.'));
295
342
  console.log();
@@ -297,7 +344,6 @@ export class DesktopServer {
297
344
  const platform = process.platform;
298
345
  try {
299
346
  if (platform === 'win32') {
300
- // Try launching Microsoft Edge or Chrome in standalone App Mode window
301
347
  spawn('cmd.exe', ['/c', 'start', 'msedge', `--app=${url}`], { detached: true, stdio: 'ignore' });
302
348
  }
303
349
  else if (platform === 'darwin') {