libfx 0.0.8 → 0.0.9-dev.982.ge26e97ec4040

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/README.md CHANGED
@@ -325,9 +325,25 @@ const runtime = await createFxTerminal({
325
325
  await runtime.interactive;
326
326
  ```
327
327
 
328
+ The xterm adapter preserves browser-style composer editing for Shift+Enter,
329
+ Command+A, Command+C, Command+X, Command+Z, and Command+Shift+Z. Shift+Enter
330
+ inserts a newline without submitting. A click inside the visible composer moves
331
+ its caret; pointer drags remain xterm terminal-output selections.
332
+ When xterm already has an output selection, Command+C copies that selection
333
+ instead of the composer selection.
334
+
328
335
  The terminal runtime exposes `interactive`, `exited`, `write`, `resize`, and
329
- `abort`. Terminal session, config, OAuth, prompt-history, URL, and workspace
330
- stores remain terminal-only host integrations.
336
+ `abort`. Terminal session, config, OAuth, prompt-history, clipboard, URL, and
337
+ workspace stores remain terminal-only host integrations. Clipboard copy writes
338
+ through the host `clipboard.writeText(text)` adapter and defaults to
339
+ `navigator.clipboard`.
340
+
341
+ During `/compact` and automatic compaction, the terminal shows a live
342
+ `Compacting` activity row with elapsed time. Input and cancellation remain
343
+ responsive while the summary request is pending. Compaction progress and
344
+ outcomes do not add transcript entries, including cancellation after resume.
345
+ Stored snapshots retain cancellation-origin metadata; keep them opaque and
346
+ resume with the same or a newer SDK build. Older snapshots remain readable.
331
347
 
332
348
  ## Security
333
349
 
package/fx-core.wasm CHANGED
Binary file
package/fx-sdk.js CHANGED
@@ -36,6 +36,7 @@ function validateGatewayChatUrl(value) {
36
36
  if (url.username || url.password || url.hash) {
37
37
  throw new TypeError("gatewayChatUrl must not contain credentials or a fragment");
38
38
  }
39
+ if (url.href === "https://ai-gateway.vercel.sh/v4/ai/language-model") return;
39
40
  if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
40
41
  const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
41
42
  if (url.protocol !== "http:" || !loopback || !url.port) {
@@ -208,14 +209,83 @@ export function encodeXtermKeyEvent(event) {
208
209
  if (event.key === "Backspace") return `\x1b[127;${modifiers + 1}u`;
209
210
  const arrow = { ArrowUp: "A", ArrowDown: "B", ArrowRight: "C", ArrowLeft: "D" }[event.key];
210
211
  if (arrow) return `\x1b[1;${modifiers + 1}${arrow}`;
212
+ const shortcut = { a: 97, c: 99, x: 120, z: 122 }[event.key.toLowerCase()];
213
+ if (shortcut) return `\x1b[${shortcut};${modifiers + 1}u`;
211
214
  }
212
215
  return null;
213
216
  }
214
217
 
218
+ function xtermPointerCell(term, event) {
219
+ const root = term.element;
220
+ const screen = root?.querySelector?.(".xterm-screen") || root;
221
+ const rect = screen?.getBoundingClientRect?.();
222
+ if (!rect || rect.width <= 0 || rect.height <= 0 || term.cols <= 0 || term.rows <= 0) return null;
223
+ if (event.clientX < rect.left || event.clientX >= rect.right ||
224
+ event.clientY < rect.top || event.clientY >= rect.bottom) return null;
225
+ return {
226
+ column: Math.min(term.cols, Math.floor((event.clientX - rect.left) * term.cols / rect.width) + 1),
227
+ row: Math.min(term.rows, Math.floor((event.clientY - rect.top) * term.rows / rect.height) + 1),
228
+ };
229
+ }
230
+
231
+ function installXtermClickHandler(term, callback) {
232
+ const element = term.element;
233
+ if (typeof element?.addEventListener !== "function") return () => {};
234
+ let pointerDown = null;
235
+ const down = (event) => {
236
+ if (event.button !== 0) return;
237
+ pointerDown = { id: event.pointerId, x: event.clientX, y: event.clientY };
238
+ };
239
+ const up = (event) => {
240
+ const start = pointerDown;
241
+ pointerDown = null;
242
+ if (!start || event.button !== 0 || event.pointerId !== start.id ||
243
+ event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
244
+ const dx = event.clientX - start.x;
245
+ const dy = event.clientY - start.y;
246
+ if (dx * dx + dy * dy > 16) return;
247
+ if (term.modes?.mouseTrackingMode && term.modes.mouseTrackingMode !== "none") return;
248
+ const cell = xtermPointerCell(term, event);
249
+ if (!cell) return;
250
+ callback(`\x1b[<0;${cell.column};${cell.row}M\x1b[<0;${cell.column};${cell.row}m`);
251
+ };
252
+ const cancel = () => { pointerDown = null; };
253
+ element.addEventListener("pointerdown", down);
254
+ element.addEventListener("pointerup", up);
255
+ element.addEventListener("pointercancel", cancel);
256
+ return () => {
257
+ element.removeEventListener("pointerdown", down);
258
+ element.removeEventListener("pointerup", up);
259
+ element.removeEventListener("pointercancel", cancel);
260
+ };
261
+ }
262
+
263
+ function installXtermShortcutHandler(term, callback) {
264
+ const element = term.element;
265
+ if (typeof element?.addEventListener !== "function") return () => {};
266
+ const keydown = (event) => {
267
+ if (xtermSelectionOwnsShortcut(term, event)) return;
268
+ const data = encodeXtermKeyEvent(event);
269
+ if (data === null) return;
270
+ event.preventDefault();
271
+ event.stopImmediatePropagation();
272
+ callback(data);
273
+ };
274
+ element.addEventListener("keydown", keydown, true);
275
+ return () => element.removeEventListener("keydown", keydown, true);
276
+ }
277
+
278
+ function xtermSelectionOwnsShortcut(term, event) {
279
+ return event.type === "keydown" && event.metaKey &&
280
+ (event.key.toLowerCase() === "c" || event.key.toLowerCase() === "x") &&
281
+ term.hasSelection?.();
282
+ }
283
+
215
284
  export function xtermAdapter(term) {
216
285
  let keyDataHandler = null;
217
286
  if (typeof term.attachCustomKeyEventHandler === "function") {
218
287
  term.attachCustomKeyEventHandler((event) => {
288
+ if (xtermSelectionOwnsShortcut(term, event)) return true;
219
289
  const data = encodeXtermKeyEvent(event);
220
290
  if (data === null || keyDataHandler === null) return true;
221
291
  keyDataHandler(data);
@@ -227,7 +297,13 @@ export function xtermAdapter(term) {
227
297
  onData(callback) { const disposable = term.onData(callback); return () => disposable.dispose(); },
228
298
  onKeyData(callback) {
229
299
  keyDataHandler = callback;
230
- return () => { if (keyDataHandler === callback) keyDataHandler = null; };
300
+ const removeShortcutHandler = installXtermShortcutHandler(term, callback);
301
+ const removeClickHandler = installXtermClickHandler(term, callback);
302
+ return () => {
303
+ removeShortcutHandler();
304
+ removeClickHandler();
305
+ if (keyDataHandler === callback) keyDataHandler = null;
306
+ };
231
307
  },
232
308
  get cols() { return term.cols; },
233
309
  get rows() { return term.rows; },
@@ -607,6 +683,23 @@ function createRuntime(options) {
607
683
  accepted === false ? 0 : 1).catch(() => 0);
608
684
  }
609
685
 
686
+ function clipboardCopy(valuePtr, valueLen) {
687
+ let clipboard = options.clipboard;
688
+ if (clipboard === undefined) {
689
+ try { clipboard = globalThis.navigator?.clipboard; } catch { clipboard = null; }
690
+ }
691
+ if (typeof clipboard?.writeText !== "function") return Promise.resolve(0);
692
+ const value = text(valuePtr, valueLen);
693
+ return Promise.resolve().then(() => clipboard.writeText(value)).then((accepted) => {
694
+ if (accepted === false) return 0;
695
+ options.emit?.("clipboard.copy", { length: value.length });
696
+ return 1;
697
+ }).catch((error) => {
698
+ options.emit?.("clipboard.copy_error", { error });
699
+ return 0;
700
+ });
701
+ }
702
+
610
703
  function oauthSessionLoad(outPtr, outCap, revisionPtr, revisionCap, revisionLenOut) {
611
704
  if (!options.oauthSessionStore?.load) return -1;
612
705
  return Promise.resolve().then(() => options.oauthSessionStore.load()).then((record) => {
@@ -913,6 +1006,7 @@ function createRuntime(options) {
913
1006
 
914
1007
  const fx = {
915
1008
  fx_term_poll_input: new WebAssembly.Suspending(termPollInput),
1009
+ fx_clipboard_copy: new WebAssembly.Suspending(clipboardCopy),
916
1010
  fx_prompt_history_available() { return options.promptHistoryStore ? 1 : 0; },
917
1011
  fx_workspace_available() { return workspace.present ? 1 : 0; },
918
1012
  fx_workspace_info: workspaceInfo,
package/fx-term.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
package/node.cjs CHANGED
@@ -218,6 +218,8 @@ function validateGatewayChatUrl(value) {
218
218
  if (url.username || url.password || url.hash) {
219
219
  throw new TypeError("gatewayChatUrl must not contain credentials or a fragment");
220
220
  }
221
+ if (url.href === "https://ai-gateway.vercel.sh/v4/ai/language-model")
222
+ return;
221
223
  if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model")
222
224
  return;
223
225
  const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
@@ -393,13 +395,89 @@ function encodeXtermKeyEvent(event) {
393
395
  const arrow = { ArrowUp: "A", ArrowDown: "B", ArrowRight: "C", ArrowLeft: "D" }[event.key];
394
396
  if (arrow)
395
397
  return `\x1B[1;${modifiers + 1}${arrow}`;
398
+ const shortcut = { a: 97, c: 99, x: 120, z: 122 }[event.key.toLowerCase()];
399
+ if (shortcut)
400
+ return `\x1B[${shortcut};${modifiers + 1}u`;
396
401
  }
397
402
  return null;
398
403
  }
404
+ function xtermPointerCell(term, event) {
405
+ const root = term.element;
406
+ const screen = root?.querySelector?.(".xterm-screen") || root;
407
+ const rect = screen?.getBoundingClientRect?.();
408
+ if (!rect || rect.width <= 0 || rect.height <= 0 || term.cols <= 0 || term.rows <= 0)
409
+ return null;
410
+ if (event.clientX < rect.left || event.clientX >= rect.right || event.clientY < rect.top || event.clientY >= rect.bottom)
411
+ return null;
412
+ return {
413
+ column: Math.min(term.cols, Math.floor((event.clientX - rect.left) * term.cols / rect.width) + 1),
414
+ row: Math.min(term.rows, Math.floor((event.clientY - rect.top) * term.rows / rect.height) + 1)
415
+ };
416
+ }
417
+ function installXtermClickHandler(term, callback) {
418
+ const element = term.element;
419
+ if (typeof element?.addEventListener !== "function")
420
+ return () => {};
421
+ let pointerDown = null;
422
+ const down = (event) => {
423
+ if (event.button !== 0)
424
+ return;
425
+ pointerDown = { id: event.pointerId, x: event.clientX, y: event.clientY };
426
+ };
427
+ const up = (event) => {
428
+ const start = pointerDown;
429
+ pointerDown = null;
430
+ if (!start || event.button !== 0 || event.pointerId !== start.id || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)
431
+ return;
432
+ const dx = event.clientX - start.x;
433
+ const dy = event.clientY - start.y;
434
+ if (dx * dx + dy * dy > 16)
435
+ return;
436
+ if (term.modes?.mouseTrackingMode && term.modes.mouseTrackingMode !== "none")
437
+ return;
438
+ const cell = xtermPointerCell(term, event);
439
+ if (!cell)
440
+ return;
441
+ callback(`\x1B[<0;${cell.column};${cell.row}M\x1B[<0;${cell.column};${cell.row}m`);
442
+ };
443
+ const cancel = () => {
444
+ pointerDown = null;
445
+ };
446
+ element.addEventListener("pointerdown", down);
447
+ element.addEventListener("pointerup", up);
448
+ element.addEventListener("pointercancel", cancel);
449
+ return () => {
450
+ element.removeEventListener("pointerdown", down);
451
+ element.removeEventListener("pointerup", up);
452
+ element.removeEventListener("pointercancel", cancel);
453
+ };
454
+ }
455
+ function installXtermShortcutHandler(term, callback) {
456
+ const element = term.element;
457
+ if (typeof element?.addEventListener !== "function")
458
+ return () => {};
459
+ const keydown = (event) => {
460
+ if (xtermSelectionOwnsShortcut(term, event))
461
+ return;
462
+ const data = encodeXtermKeyEvent(event);
463
+ if (data === null)
464
+ return;
465
+ event.preventDefault();
466
+ event.stopImmediatePropagation();
467
+ callback(data);
468
+ };
469
+ element.addEventListener("keydown", keydown, true);
470
+ return () => element.removeEventListener("keydown", keydown, true);
471
+ }
472
+ function xtermSelectionOwnsShortcut(term, event) {
473
+ return event.type === "keydown" && event.metaKey && (event.key.toLowerCase() === "c" || event.key.toLowerCase() === "x") && term.hasSelection?.();
474
+ }
399
475
  function xtermAdapter(term) {
400
476
  let keyDataHandler = null;
401
477
  if (typeof term.attachCustomKeyEventHandler === "function") {
402
478
  term.attachCustomKeyEventHandler((event) => {
479
+ if (xtermSelectionOwnsShortcut(term, event))
480
+ return true;
403
481
  const data = encodeXtermKeyEvent(event);
404
482
  if (data === null || keyDataHandler === null)
405
483
  return true;
@@ -417,7 +495,11 @@ function xtermAdapter(term) {
417
495
  },
418
496
  onKeyData(callback) {
419
497
  keyDataHandler = callback;
498
+ const removeShortcutHandler = installXtermShortcutHandler(term, callback);
499
+ const removeClickHandler = installXtermClickHandler(term, callback);
420
500
  return () => {
501
+ removeShortcutHandler();
502
+ removeClickHandler();
421
503
  if (keyDataHandler === callback)
422
504
  keyDataHandler = null;
423
505
  };
@@ -844,6 +926,28 @@ function createRuntime(options) {
844
926
  return 0;
845
927
  return Promise.resolve().then(() => options.openUrl(text(urlPtr, urlLen))).then((accepted) => accepted === false ? 0 : 1).catch(() => 0);
846
928
  }
929
+ function clipboardCopy(valuePtr, valueLen) {
930
+ let clipboard = options.clipboard;
931
+ if (clipboard === undefined) {
932
+ try {
933
+ clipboard = globalThis.navigator?.clipboard;
934
+ } catch {
935
+ clipboard = null;
936
+ }
937
+ }
938
+ if (typeof clipboard?.writeText !== "function")
939
+ return Promise.resolve(0);
940
+ const value = text(valuePtr, valueLen);
941
+ return Promise.resolve().then(() => clipboard.writeText(value)).then((accepted) => {
942
+ if (accepted === false)
943
+ return 0;
944
+ options.emit?.("clipboard.copy", { length: value.length });
945
+ return 1;
946
+ }).catch((error) => {
947
+ options.emit?.("clipboard.copy_error", { error });
948
+ return 0;
949
+ });
950
+ }
847
951
  function oauthSessionLoad(outPtr, outCap, revisionPtr, revisionCap, revisionLenOut) {
848
952
  if (!options.oauthSessionStore?.load)
849
953
  return -1;
@@ -1216,6 +1320,7 @@ function createRuntime(options) {
1216
1320
  };
1217
1321
  const fx = {
1218
1322
  fx_term_poll_input: new WebAssembly.Suspending(termPollInput),
1323
+ fx_clipboard_copy: new WebAssembly.Suspending(clipboardCopy),
1219
1324
  fx_prompt_history_available() {
1220
1325
  return options.promptHistoryStore ? 1 : 0;
1221
1326
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.8",
3
+ "version": "0.0.9-dev.982.ge26e97ec4040",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {