copilot-liku-cli 0.0.8 → 0.0.10

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,83 @@
1
+ /**
2
+ * Agent Trace Writer — persistent JSONL flight recorder
3
+ *
4
+ * Subscribes to orchestrator events and writes a structured trace log
5
+ * to ~/.liku-cli/traces/<sessionId>.jsonl for post-hoc debugging.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const TRACE_DIR = path.join(os.homedir(), '.liku-cli', 'traces');
13
+
14
+ class TraceWriter {
15
+ constructor(orchestrator) {
16
+ this.orchestrator = orchestrator;
17
+ this.stream = null;
18
+ this.sessionId = null;
19
+
20
+ this._bindEvents();
21
+ }
22
+
23
+ _ensureDir() {
24
+ if (!fs.existsSync(TRACE_DIR)) {
25
+ fs.mkdirSync(TRACE_DIR, { recursive: true, mode: 0o700 });
26
+ }
27
+ }
28
+
29
+ _write(event, data) {
30
+ if (!this.stream) return;
31
+ const entry = {
32
+ ts: new Date().toISOString(),
33
+ session: this.sessionId,
34
+ event,
35
+ ...data
36
+ };
37
+ this.stream.write(JSON.stringify(entry) + '\n');
38
+ }
39
+
40
+ _bindEvents() {
41
+ const o = this.orchestrator;
42
+
43
+ o.on('session:start', (session) => {
44
+ this._ensureDir();
45
+ this.sessionId = session.id;
46
+ const filePath = path.join(TRACE_DIR, `${this.sessionId}.jsonl`);
47
+ this.stream = fs.createWriteStream(filePath, { flags: 'a', mode: 0o600 });
48
+ this._write('session:start', { metadata: session.metadata });
49
+ });
50
+
51
+ o.on('session:end', (session) => {
52
+ this._write('session:end', { summary: session.summary });
53
+ this._close();
54
+ });
55
+
56
+ o.on('task:start', (d) => this._write('task:start', { task: d.task, agent: d.agent }));
57
+ o.on('task:complete', (d) => this._write('task:complete', { success: d.result?.success }));
58
+ o.on('task:error', (d) => this._write('task:error', { error: d.error?.message || String(d.error) }));
59
+ o.on('handoff:execute', (h) => this._write('handoff', { from: h.from, to: h.to, message: h.message }));
60
+ o.on('checkpoint', (cp) => this._write('checkpoint', { label: cp.label }));
61
+
62
+ // Agent-level events
63
+ o.on('agent:log', (entry) => this._write('agent:log', entry));
64
+ o.on('agent:proof', (proof) => this._write('agent:proof', proof));
65
+ o.on('agent:handoff', (h) => this._write('agent:handoff', h));
66
+ }
67
+
68
+ _close() {
69
+ if (this.stream) {
70
+ this.stream.end();
71
+ this.stream = null;
72
+ }
73
+ this.sessionId = null;
74
+ }
75
+
76
+ /** Destroy and detach all listeners */
77
+ destroy() {
78
+ this._close();
79
+ this.orchestrator.removeAllListeners();
80
+ }
81
+ }
82
+
83
+ module.exports = { TraceWriter };
@@ -189,6 +189,214 @@ const AI_PROVIDERS = {
189
189
  // GitHub Copilot OAuth Configuration
190
190
  const COPILOT_CLIENT_ID = 'Iv1.b507a08c87ecfe98';
191
191
 
192
+ // ===== TOOL DEFINITIONS FOR NATIVE FUNCTION CALLING =====
193
+ // These map directly to the action types the system already executes.
194
+ const LIKU_TOOLS = [
195
+ {
196
+ type: 'function',
197
+ function: {
198
+ name: 'click_element',
199
+ description: 'Click a UI element by its visible text or name (uses Windows UI Automation). Preferred over coordinate clicks.',
200
+ parameters: {
201
+ type: 'object',
202
+ properties: {
203
+ text: { type: 'string', description: 'The visible text/name of the element to click' },
204
+ reason: { type: 'string', description: 'Why this click is needed' }
205
+ },
206
+ required: ['text']
207
+ }
208
+ }
209
+ },
210
+ {
211
+ type: 'function',
212
+ function: {
213
+ name: 'click',
214
+ description: 'Left click at pixel coordinates on screen. Use as fallback when click_element cannot find the target.',
215
+ parameters: {
216
+ type: 'object',
217
+ properties: {
218
+ x: { type: 'number', description: 'X pixel coordinate' },
219
+ y: { type: 'number', description: 'Y pixel coordinate' },
220
+ reason: { type: 'string', description: 'Why clicking here' }
221
+ },
222
+ required: ['x', 'y']
223
+ }
224
+ }
225
+ },
226
+ {
227
+ type: 'function',
228
+ function: {
229
+ name: 'double_click',
230
+ description: 'Double click at pixel coordinates.',
231
+ parameters: {
232
+ type: 'object',
233
+ properties: {
234
+ x: { type: 'number', description: 'X pixel coordinate' },
235
+ y: { type: 'number', description: 'Y pixel coordinate' }
236
+ },
237
+ required: ['x', 'y']
238
+ }
239
+ }
240
+ },
241
+ {
242
+ type: 'function',
243
+ function: {
244
+ name: 'right_click',
245
+ description: 'Right click at pixel coordinates to open context menu.',
246
+ parameters: {
247
+ type: 'object',
248
+ properties: {
249
+ x: { type: 'number', description: 'X pixel coordinate' },
250
+ y: { type: 'number', description: 'Y pixel coordinate' }
251
+ },
252
+ required: ['x', 'y']
253
+ }
254
+ }
255
+ },
256
+ {
257
+ type: 'function',
258
+ function: {
259
+ name: 'type_text',
260
+ description: 'Type text into the currently focused input field.',
261
+ parameters: {
262
+ type: 'object',
263
+ properties: {
264
+ text: { type: 'string', description: 'The text to type' }
265
+ },
266
+ required: ['text']
267
+ }
268
+ }
269
+ },
270
+ {
271
+ type: 'function',
272
+ function: {
273
+ name: 'press_key',
274
+ description: 'Press a key or keyboard shortcut (e.g., "enter", "ctrl+c", "win+r", "alt+tab").',
275
+ parameters: {
276
+ type: 'object',
277
+ properties: {
278
+ key: { type: 'string', description: 'Key combo string (e.g., "ctrl+s", "enter", "win+d")' },
279
+ reason: { type: 'string', description: 'Why pressing this key' }
280
+ },
281
+ required: ['key']
282
+ }
283
+ }
284
+ },
285
+ {
286
+ type: 'function',
287
+ function: {
288
+ name: 'scroll',
289
+ description: 'Scroll up or down.',
290
+ parameters: {
291
+ type: 'object',
292
+ properties: {
293
+ direction: { type: 'string', enum: ['up', 'down'], description: 'Scroll direction' },
294
+ amount: { type: 'number', description: 'Scroll amount (default 3)' }
295
+ },
296
+ required: ['direction']
297
+ }
298
+ }
299
+ },
300
+ {
301
+ type: 'function',
302
+ function: {
303
+ name: 'drag',
304
+ description: 'Drag from one point to another.',
305
+ parameters: {
306
+ type: 'object',
307
+ properties: {
308
+ fromX: { type: 'number' }, fromY: { type: 'number' },
309
+ toX: { type: 'number' }, toY: { type: 'number' }
310
+ },
311
+ required: ['fromX', 'fromY', 'toX', 'toY']
312
+ }
313
+ }
314
+ },
315
+ {
316
+ type: 'function',
317
+ function: {
318
+ name: 'wait',
319
+ description: 'Wait for a specified number of milliseconds before the next action.',
320
+ parameters: {
321
+ type: 'object',
322
+ properties: {
323
+ ms: { type: 'number', description: 'Milliseconds to wait' }
324
+ },
325
+ required: ['ms']
326
+ }
327
+ }
328
+ },
329
+ {
330
+ type: 'function',
331
+ function: {
332
+ name: 'screenshot',
333
+ description: 'Take a screenshot to see the current screen state. Use for verification or when elements are not in the UI tree.',
334
+ parameters: { type: 'object', properties: {} }
335
+ }
336
+ },
337
+ {
338
+ type: 'function',
339
+ function: {
340
+ name: 'run_command',
341
+ description: 'Execute a shell command and return output. Preferred for any file/system operations.',
342
+ parameters: {
343
+ type: 'object',
344
+ properties: {
345
+ command: { type: 'string', description: 'Shell command to execute' },
346
+ cwd: { type: 'string', description: 'Working directory (optional)' },
347
+ shell: { type: 'string', enum: ['powershell', 'cmd', 'bash'], description: 'Shell to use (default: powershell on Windows)' }
348
+ },
349
+ required: ['command']
350
+ }
351
+ }
352
+ },
353
+ {
354
+ type: 'function',
355
+ function: {
356
+ name: 'focus_window',
357
+ description: 'Bring a window to the foreground by its handle or title.',
358
+ parameters: {
359
+ type: 'object',
360
+ properties: {
361
+ title: { type: 'string', description: 'Partial window title to match' },
362
+ windowHandle: { type: 'number', description: 'Window handle (hwnd)' }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ ];
368
+
369
+ /**
370
+ * Convert tool_calls from API response into the action block format
371
+ * that the existing executeActions pipeline expects.
372
+ */
373
+ function toolCallsToActions(toolCalls) {
374
+ return toolCalls.map(tc => {
375
+ let args;
376
+ try { args = JSON.parse(tc.function.arguments); } catch { args = {}; }
377
+ const name = tc.function.name;
378
+
379
+ // Map tool names back to existing action types
380
+ switch (name) {
381
+ case 'click_element': return { type: 'click_element', ...args };
382
+ case 'click': return { type: 'click', ...args };
383
+ case 'double_click': return { type: 'double_click', ...args };
384
+ case 'right_click': return { type: 'right_click', ...args };
385
+ case 'type_text': return { type: 'type', ...args };
386
+ case 'press_key': return { type: 'key', key: args.key, reason: args.reason };
387
+ case 'scroll': return { type: 'scroll', ...args };
388
+ case 'drag': return { type: 'drag', ...args };
389
+ case 'wait': return { type: 'wait', ...args };
390
+ case 'screenshot': return { type: 'screenshot' };
391
+ case 'run_command': return { type: 'run_command', ...args };
392
+ case 'focus_window':
393
+ if (args.title) return { type: 'bring_window_to_front', title: args.title };
394
+ return { type: 'focus_window', windowHandle: args.windowHandle };
395
+ default: return { type: name, ...args };
396
+ }
397
+ });
398
+ }
399
+
192
400
  // Current configuration
193
401
  let currentProvider = 'copilot'; // Default to GitHub Copilot
194
402
  let apiKeys = {
@@ -218,6 +426,41 @@ let oauthCallback = null;
218
426
  // Conversation history for context
219
427
  let conversationHistory = [];
220
428
  const MAX_HISTORY = 20;
429
+ const HISTORY_FILE = path.join(LIKU_HOME, 'conversation-history.json');
430
+
431
+ /**
432
+ * Load conversation history from disk (survives process restarts)
433
+ */
434
+ function loadConversationHistory() {
435
+ try {
436
+ if (fs.existsSync(HISTORY_FILE)) {
437
+ const data = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
438
+ if (Array.isArray(data)) {
439
+ conversationHistory = data.slice(-MAX_HISTORY * 2);
440
+ console.log(`[AI] Restored ${conversationHistory.length} history entries from disk`);
441
+ }
442
+ }
443
+ } catch (e) {
444
+ console.warn('[AI] Could not load conversation history:', e.message);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Persist conversation history to disk
450
+ */
451
+ function saveConversationHistory() {
452
+ try {
453
+ if (!fs.existsSync(LIKU_HOME)) {
454
+ fs.mkdirSync(LIKU_HOME, { recursive: true, mode: 0o700 });
455
+ }
456
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(conversationHistory.slice(-MAX_HISTORY * 2)), { mode: 0o600 });
457
+ } catch (e) {
458
+ console.warn('[AI] Could not save conversation history:', e.message);
459
+ }
460
+ }
461
+
462
+ // Restore history on module load
463
+ loadConversationHistory();
221
464
 
222
465
  // Visual context for AI awareness
223
466
  let visualContextBuffer = [];
@@ -243,10 +486,38 @@ function getPlatformContext() {
243
486
  - **Screenshot**: \`win+shift+s\`
244
487
 
245
488
  ### Windows Terminal Shortcuts
246
- - **New tab**: \`ctrl+shift+t\`
247
- - **Close tab**: \`ctrl+shift+w\`
489
+ - (Windows Terminal only) **New tab**: \`ctrl+shift+t\`
490
+ - (Windows Terminal only) **Close tab**: \`ctrl+shift+w\`
248
491
  - **Split pane**: \`alt+shift+d\`
249
492
 
493
+ ### Browser Tab Shortcuts (Edge/Chrome)
494
+ - **New tab**: \`ctrl+t\`
495
+ - **Close tab**: \`ctrl+w\`
496
+ - **Reopen closed tab**: \`ctrl+shift+t\`
497
+ - **Close window**: \`ctrl+shift+w\`
498
+
499
+ ### Focus Rule (CRITICAL)
500
+ Before sending keyboard shortcuts, make sure the intended app window is focused.
501
+ If the overlay/chat has focus, shortcuts like \`ctrl+w\` / \`ctrl+shift+w\` may close the overlay instead of the target app.
502
+
503
+ ### Target Verification (CRITICAL)
504
+ - For any action that affects a specific app (especially browsers), **verify the active window is correct before executing**.
505
+ - Prefer this sequence:
506
+ 1) Bring the target window to front (e.g., Edge)
507
+ 2) Confirm active window (title/process)
508
+ 3) Only then send keys/clicks
509
+ - If unsure, take a screenshot for confirmation.
510
+
511
+ ### Browser Tab Targeting (Edge/Chrome)
512
+ - You generally **cannot safely close a specific tab by title** unless you first make that tab active.
513
+ - Prefer:
514
+ 1) Focus Edge/Chrome window
515
+ 2) Activate the tab by clicking its title in the tab strip (UIA or coordinate click)
516
+ 3) Then close tab with \`ctrl+w\`
517
+ - If the tab title is not discoverable via UI Automation, use keyboard strategies:
518
+ - \`ctrl+1..8\` switch to tab 1..8, \`ctrl+9\` switches to last tab
519
+ - \`ctrl+tab\` / \`ctrl+shift+tab\` cycle tabs (add waits)
520
+
250
521
  ### IMPORTANT: On Windows, NEVER use:
251
522
  - \`cmd+space\` (that's macOS Spotlight)
252
523
  - \`ctrl+alt+t\` (that's Linux terminal shortcut)`;
@@ -366,11 +637,15 @@ When the user asks you to DO something, respond with a JSON action block:
366
637
  - Be specific about UI elements, text, buttons
367
638
 
368
639
  **For ACTION requests** (click here, type this, open that):
369
- - ALWAYS respond with the JSON action block
640
+ - **YOU MUST respond with the JSON action block — NEVER respond with only a plan or description**
641
+ - **NEVER say "Let me proceed" or "I will click" without including the actual \`\`\`json action block**
642
+ - **If the user says "proceed" or "do it", output the JSON actions immediately — do not ask again**
370
643
  - Use PLATFORM-SPECIFIC shortcuts (see above!)
371
644
  - Prefer \`click_element\` over coordinate clicks when targeting named UI elements
372
645
  - Add \`wait\` actions between steps that need UI to update
373
646
  - Add verification step to confirm success
647
+ - **If an element is NOT in the Live UI State**: Use \`{"type": "screenshot"}\` first, then use coordinates from the screenshot to click. Do NOT give up or say "I can't find the element."
648
+ - **If you need to interact with web content inside an app** (like VS Code panels, browser tabs): Use keyboard shortcuts or coordinate-based clicks since web UI may not appear in UIA tree
374
649
 
375
650
  **Common Task Patterns**:
376
651
  ${PLATFORM === 'win32' ? `
@@ -391,7 +666,14 @@ ${PLATFORM === 'win32' ? `
391
666
  - **Save file**: \`ctrl+s\`
392
667
  - **Copy/Paste**: \`ctrl+c\` / \`ctrl+v\``}
393
668
 
394
- Be precise, use platform-correct shortcuts, and execute actions confidently!`;
669
+ Be precise, use platform-correct shortcuts, and execute actions confidently!
670
+
671
+ ## CRITICAL RULES
672
+ 1. **NEVER describe actions without executing them.** If the user asks you to click/type/open something, output the JSON action block.
673
+ 2. **NEVER say "Let me proceed" or "I'll do this now" without the JSON block.** Words without actions are useless.
674
+ 3. **If user says "proceed" or "go ahead", output the JSON actions IMMEDIATELY.**
675
+ 4. **When you can't find an element in Live UI State, take a screenshot and use pixel coordinates.** Don't give up.
676
+ 5. **One response = one action block.** Don't split actions across multiple messages unless the user asks you to wait.`;
395
677
 
396
678
  /**
397
679
  * Set the AI provider
@@ -668,12 +950,12 @@ function saveCopilotToken(token) {
668
950
  try {
669
951
  const dir = path.dirname(TOKEN_FILE);
670
952
  if (!fs.existsSync(dir)) {
671
- fs.mkdirSync(dir, { recursive: true });
953
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
672
954
  }
673
955
  fs.writeFileSync(TOKEN_FILE, JSON.stringify({
674
956
  access_token: token,
675
957
  saved_at: new Date().toISOString()
676
- }));
958
+ }), { mode: 0o600 });
677
959
  console.log('[COPILOT] Token saved');
678
960
  } catch (e) {
679
961
  console.error('[COPILOT] Failed to save token:', e.message);
@@ -920,7 +1202,9 @@ async function callCopilot(messages, modelOverride = null) {
920
1202
  messages: messages,
921
1203
  max_tokens: 4096,
922
1204
  temperature: 0.7,
923
- stream: false
1205
+ stream: false,
1206
+ tools: LIKU_TOOLS,
1207
+ tool_choice: 'auto'
924
1208
  });
925
1209
 
926
1210
  // Try multiple endpoint formats
@@ -979,7 +1263,22 @@ async function callCopilot(messages, modelOverride = null) {
979
1263
  try {
980
1264
  const result = JSON.parse(body);
981
1265
  if (result.choices && result.choices[0]) {
982
- resolveReq(result.choices[0].message.content);
1266
+ const choice = result.choices[0];
1267
+ const msg = choice.message;
1268
+
1269
+ // Handle native tool calls — convert to action JSON block
1270
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
1271
+ const actions = toolCallsToActions(msg.tool_calls);
1272
+ const actionBlock = JSON.stringify({
1273
+ thought: msg.content || 'Executing requested actions',
1274
+ actions,
1275
+ verification: 'Verify the actions completed successfully'
1276
+ }, null, 2);
1277
+ console.log(`[Copilot] Received ${msg.tool_calls.length} tool_calls, converted to action block`);
1278
+ resolveReq('```json\n' + actionBlock + '\n```');
1279
+ } else {
1280
+ resolveReq(msg.content);
1281
+ }
983
1282
  } else if (result.error) {
984
1283
  rejectReq(new Error(result.error.message || 'Copilot API error'));
985
1284
  } else {
@@ -1237,6 +1536,9 @@ function detectTruncation(response) {
1237
1536
  /**
1238
1537
  * Send a message and get AI response with auto-continuation
1239
1538
  */
1539
+ // Provider fallback priority order
1540
+ const PROVIDER_FALLBACK_ORDER = ['copilot', 'openai', 'anthropic', 'ollama'];
1541
+
1240
1542
  async function sendMessage(userMessage, options = {}) {
1241
1543
  const { includeVisualContext = false, coordinates = null, maxContinuations = 2, model = null } = options;
1242
1544
 
@@ -1253,43 +1555,55 @@ async function sendMessage(userMessage, options = {}) {
1253
1555
  let response;
1254
1556
  let effectiveModel = currentCopilotModel;
1255
1557
 
1256
- switch (currentProvider) {
1257
- case 'copilot':
1258
- // GitHub Copilot - uses OAuth token or env var
1259
- if (!apiKeys.copilot) {
1260
- // Try loading saved token
1261
- if (!loadCopilotToken()) {
1262
- throw new Error('Not authenticated with GitHub Copilot.\n\nTo authenticate:\n1. Type /login and authorize in browser\n2. Or set GH_TOKEN or GITHUB_TOKEN environment variable');
1263
- }
1264
- }
1265
- effectiveModel = resolveCopilotModelKey(model);
1266
- // Enforce vision-capable model when visual context is included
1267
- if (includeVisualContext && COPILOT_MODELS[effectiveModel] && !COPILOT_MODELS[effectiveModel].vision) {
1268
- const visionFallback = AI_PROVIDERS.copilot.visionModel || 'gpt-4o';
1269
- console.log(`[AI] Model ${effectiveModel} lacks vision, upgrading to ${visionFallback} for visual context`);
1270
- effectiveModel = visionFallback;
1271
- }
1272
- response = await callCopilot(messages, effectiveModel);
1273
- break;
1274
-
1275
- case 'openai':
1276
- if (!apiKeys.openai) {
1277
- throw new Error('OpenAI API key not set. Use /setkey openai <key> or set OPENAI_API_KEY environment variable.');
1558
+ // Build fallback chain: current provider first, then remaining in priority order
1559
+ const fallbackChain = [currentProvider, ...PROVIDER_FALLBACK_ORDER.filter(p => p !== currentProvider)];
1560
+ let lastError = null;
1561
+ let usedProvider = currentProvider;
1562
+
1563
+ for (const provider of fallbackChain) {
1564
+ try {
1565
+ switch (provider) {
1566
+ case 'copilot':
1567
+ if (!apiKeys.copilot) {
1568
+ if (!loadCopilotToken()) {
1569
+ throw new Error('Not authenticated with GitHub Copilot.');
1570
+ }
1571
+ }
1572
+ effectiveModel = resolveCopilotModelKey(model);
1573
+ if (includeVisualContext && COPILOT_MODELS[effectiveModel] && !COPILOT_MODELS[effectiveModel].vision) {
1574
+ const visionFallback = AI_PROVIDERS.copilot.visionModel || 'gpt-4o';
1575
+ console.log(`[AI] Model ${effectiveModel} lacks vision, upgrading to ${visionFallback} for visual context`);
1576
+ effectiveModel = visionFallback;
1577
+ }
1578
+ response = await callCopilot(messages, effectiveModel);
1579
+ break;
1580
+ case 'openai':
1581
+ if (!apiKeys.openai) throw new Error('OpenAI API key not set.');
1582
+ response = await callOpenAI(messages);
1583
+ break;
1584
+ case 'anthropic':
1585
+ if (!apiKeys.anthropic) throw new Error('Anthropic API key not set.');
1586
+ response = await callAnthropic(messages);
1587
+ break;
1588
+ case 'ollama':
1589
+ default:
1590
+ response = await callOllama(messages);
1591
+ break;
1278
1592
  }
1279
- response = await callOpenAI(messages);
1280
- break;
1281
-
1282
- case 'anthropic':
1283
- if (!apiKeys.anthropic) {
1284
- throw new Error('Anthropic API key not set. Use /setkey anthropic <key> or set ANTHROPIC_API_KEY environment variable.');
1593
+ usedProvider = provider;
1594
+ if (usedProvider !== currentProvider) {
1595
+ console.log(`[AI] Fallback: ${currentProvider} failed, succeeded with ${usedProvider}`);
1285
1596
  }
1286
- response = await callAnthropic(messages);
1287
- break;
1288
-
1289
- case 'ollama':
1290
- default:
1291
- response = await callOllama(messages);
1292
- break;
1597
+ break; // success — exit fallback loop
1598
+ } catch (providerErr) {
1599
+ lastError = providerErr;
1600
+ console.warn(`[AI] Provider ${provider} failed: ${providerErr.message}`);
1601
+ continue; // try next provider
1602
+ }
1603
+ }
1604
+
1605
+ if (!response) {
1606
+ throw lastError || new Error('All AI providers failed.');
1293
1607
  }
1294
1608
 
1295
1609
  // Auto-continuation for truncated responses
@@ -1345,10 +1659,13 @@ async function sendMessage(userMessage, options = {}) {
1345
1659
  conversationHistory.shift();
1346
1660
  }
1347
1661
 
1662
+ // Persist to disk for session continuity
1663
+ saveConversationHistory();
1664
+
1348
1665
  return {
1349
1666
  success: true,
1350
1667
  message: response,
1351
- provider: currentProvider,
1668
+ provider: usedProvider,
1352
1669
  model: effectiveModel,
1353
1670
  modelVersion: COPILOT_MODELS[effectiveModel]?.id || null,
1354
1671
  hasVisualContext: includeVisualContext && visualContextBuffer.length > 0
@@ -1393,6 +1710,7 @@ function handleCommand(command) {
1393
1710
  case '/clear':
1394
1711
  conversationHistory = [];
1395
1712
  clearVisualContext();
1713
+ saveConversationHistory();
1396
1714
  return { type: 'system', message: 'Conversation and visual context cleared.' };
1397
1715
 
1398
1716
  case '/vision':
@@ -2038,5 +2356,8 @@ module.exports = {
2038
2356
  setUIWatcher,
2039
2357
  getUIWatcher,
2040
2358
  setSemanticDOMSnapshot,
2041
- clearSemanticDOMSnapshot
2359
+ clearSemanticDOMSnapshot,
2360
+ // Tool-calling
2361
+ LIKU_TOOLS,
2362
+ toolCallsToActions
2042
2363
  };
package/src/main/index.js CHANGED
@@ -344,6 +344,7 @@ function createOverlayWindow() {
344
344
  webPreferences: {
345
345
  nodeIntegration: false,
346
346
  contextIsolation: true,
347
+ sandbox: true,
347
348
  preload: path.join(__dirname, '../renderer/overlay/preload.js')
348
349
  }
349
350
  });
@@ -460,6 +461,7 @@ function createChatWindow() {
460
461
  webPreferences: {
461
462
  nodeIntegration: false,
462
463
  contextIsolation: true,
464
+ sandbox: true,
463
465
  preload: path.join(__dirname, '../renderer/chat/preload.js')
464
466
  }
465
467
  });