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 +18 -2
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +95 -1
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/node.cjs +105 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
package/libfx.darwin-arm64.node
CHANGED
|
Binary file
|
package/libfx.darwin-x64.node
CHANGED
|
Binary file
|
package/libfx.linux-arm64.node
CHANGED
|
Binary file
|
package/libfx.linux-x64.node
CHANGED
|
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
|
},
|