From 3112523f731d84d01bd10db5e53d733fe96ae4b3 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:24:42 -0500 Subject: [PATCH 1/4] linux: work-633 (codex) Agent-Team: linux Worker: codex --- web/fizzy.js | 23 ++++++++++++----------- web/tests/embed.test.mjs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/web/fizzy.js b/web/fizzy.js index e822da0f8..21d3d5a0e 100644 --- a/web/fizzy.js +++ b/web/fizzy.js @@ -1060,6 +1060,15 @@ export class Fizzy { window.matchMedia(q).addEventListener("change", () => post({ type: "prefs", prefs: prefs() })); } + // What the keyboard does to the fragment before the caret reaches the worker as typing. + const inputFilter = new TextInput(hidden, { + text: (text) => post({ type: "text", text }), + backspace: () => { + post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); + post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); + }, + }); + worker.onerror = (err) => { console.error(err); onError(`Failed to load the editor (${err.message || "worker error"}).`); @@ -1081,7 +1090,7 @@ export class Fizzy { canvas.style.cursor = m.name; break; case "textInput": - if ((textInputRect.length === 0) !== (m.rect.length === 0)) textInput.clear(); + if ((textInputRect.length === 0) !== (m.rect.length === 0)) inputFilter.clear(); textInputRect = m.rect; oskCheck(); break; @@ -1218,17 +1227,9 @@ export class Fizzy { axis(1, ev.deltaY, []); }, { passive: false }); - // What the keyboard does to the fragment before the caret reaches the worker as typing. - const textInput = new TextInput(hidden, { - text: (text) => post({ type: "text", text }), - backspace: () => { - post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); - post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); - }, - }); - canvas.addEventListener("pointerdown", () => textInput.clear()); + canvas.addEventListener("pointerdown", () => inputFilter.clear()); const keydown = (ev) => { - textInput.keydown(ev); + inputFilter.keydown(ev); if (ev.key === "Tab") { if (ev.ctrlKey) return; ev.preventDefault(); diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs index 6de090af0..b49501e2d 100644 --- a/web/tests/embed.test.mjs +++ b/web/tests/embed.test.mjs @@ -139,3 +139,33 @@ assert.equal(document.fullscreenElement, parentA, "leaving the second embed reve toggles[0](); assert.equal(document.fullscreenElement, null, "leaving the first embed exits full screen"); console.log("embed.test: ok"); + +// Deliver text input as soon as the handler is installed, before setup completes. +let earlyTextInputs = 0; +let filterClears = 0; +const { TextInput } = await import(pathToFileURL(path.join(dir, "web.js")).href); +TextInput.prototype.clear = () => { filterClears++; }; +Canvas.prototype.transferControlToOffscreen = function () { return {}; }; +globalThis.OffscreenCanvas = class {}; +globalThis.ResizeObserver = class { observe() {} }; +globalThis.Worker = class { + set onmessage(handler) { + this.handler = handler; + handler({ data: { type: "textInput", rect: [10, 20, 80, 30] } }); + earlyTextInputs++; + } + postMessage(message) { + if (message.type === "init") this.handler({ data: { type: "ready" } }); + } +}; +const workerCanvas = document.createElement("canvas"); +workerCanvas.parentElement = new Element(); +await new Promise((resolve, reject) => { + new Fizzy({ canvas: workerCanvas, worker: true, persist: false, onReady: resolve, onError: reject }).run(); +}); +assert.equal(earlyTextInputs, 1, "worker text input is handled during setup"); +assert.equal(filterClears, 1, "entering text input clears the initialized filter"); +assert.equal(document.body.children[0].style.left, "10px", "early text input positions the hidden input"); +workerCanvas.dispatch("pointerdown", { pointerId: 1, pointerType: "mouse", buttons: 1, clientX: 0, clientY: 0, timeStamp: 0 }); +assert.equal(filterClears, 2, "pointer input still clears the filter"); +console.log("embed.test: worker startup and early text input ok"); From 2f12172bb2063c94a5949036866b3bbfe8fcd300 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:05:32 -0500 Subject: [PATCH 2/4] linux: work-633 (codex) Agent-Team: linux Worker: codex --- web/fizzy.js | 18 +++++++++--------- web/tests/embed.test.mjs | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/web/fizzy.js b/web/fizzy.js index 709c96ca0..0bc1fae74 100644 --- a/web/fizzy.js +++ b/web/fizzy.js @@ -1037,6 +1037,15 @@ export class Fizzy { focusQuietly(hidden); } + // Initialize before worker setup can deliver text-input messages. + const inputFilter = new TextInput(hidden, { + text: (text) => post({ type: "text", text }), + backspace: () => { + post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); + post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); + }, + }); + let boundKeys = new Set(); const offscreen = canvas.transferControlToOffscreen(); // The init waits for the early compile (`wasmModule`), which by now has usually finished. @@ -1066,15 +1075,6 @@ export class Fizzy { window.matchMedia(q).addEventListener("change", () => post({ type: "prefs", prefs: prefs() })); } - // What the keyboard does to the fragment before the caret reaches the worker as typing. - const inputFilter = new TextInput(hidden, { - text: (text) => post({ type: "text", text }), - backspace: () => { - post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); - post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); - }, - }); - worker.onerror = (err) => { console.error(err); onError(`Failed to load the editor (${err.message || "worker error"}).`); diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs index b49501e2d..b139532c3 100644 --- a/web/tests/embed.test.mjs +++ b/web/tests/embed.test.mjs @@ -143,18 +143,24 @@ console.log("embed.test: ok"); // Deliver text input as soon as the handler is installed, before setup completes. let earlyTextInputs = 0; let filterClears = 0; +const filteredKeys = []; +const workerMessages = []; +let workerInstance; const { TextInput } = await import(pathToFileURL(path.join(dir, "web.js")).href); TextInput.prototype.clear = () => { filterClears++; }; +TextInput.prototype.keydown = (ev) => { filteredKeys.push(ev.key); }; Canvas.prototype.transferControlToOffscreen = function () { return {}; }; globalThis.OffscreenCanvas = class {}; globalThis.ResizeObserver = class { observe() {} }; globalThis.Worker = class { + constructor() { workerInstance = this; } set onmessage(handler) { this.handler = handler; handler({ data: { type: "textInput", rect: [10, 20, 80, 30] } }); earlyTextInputs++; } postMessage(message) { + workerMessages.push(message); if (message.type === "init") this.handler({ data: { type: "ready" } }); } }; @@ -168,4 +174,18 @@ assert.equal(filterClears, 1, "entering text input clears the initialized filter assert.equal(document.body.children[0].style.left, "10px", "early text input positions the hidden input"); workerCanvas.dispatch("pointerdown", { pointerId: 1, pointerType: "mouse", buttons: 1, clientX: 0, clientY: 0, timeStamp: 0 }); assert.equal(filterClears, 2, "pointer input still clears the filter"); +const hidden = document.body.children[0]; +assert.equal(key(hidden).defaultPrevented, false, "the worker hidden input is recognized without asking main-thread wasm"); +for (const key of ["a", "Backspace"]) { + hidden.dispatch("keydown", { key, preventDefault() {} }); + hidden.dispatch("keyup", { key }); +} +assert.deepEqual(filteredKeys, ["a", "Backspace"], "typing and backspace reach the filter after early text input"); +assert.deepEqual(workerMessages.filter((m) => m.type === "key").map((m) => [m.kind, m.key]), [ + [5, "a"], [6, "a"], [5, "Backspace"], [6, "Backspace"], +], "typing and backspace still reach the worker"); +workerInstance.handler({ data: { type: "textInput", rect: [20, 30, 80, 30] } }); +assert.equal(filterClears, 2, "moving the text input preserves its fragment"); +workerInstance.handler({ data: { type: "textInput", rect: [] } }); +assert.equal(filterClears, 3, "leaving text input clears the filter"); console.log("embed.test: worker startup and early text input ok"); From 91486148161ed52810f1e1c1393bec93c10436fe Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:53:20 -0500 Subject: [PATCH 3/4] linux: work-633 (codex) Agent-Team: linux Worker: codex --- web/fizzy.js | 3 ++- web/tests/embed.test.mjs | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/web/fizzy.js b/web/fizzy.js index 0bc1fae74..a7bff936f 100644 --- a/web/fizzy.js +++ b/web/fizzy.js @@ -1037,7 +1037,8 @@ export class Fizzy { focusQuietly(hidden); } - // Initialize before worker setup can deliver text-input messages. + // Forward keyboard edits to the fragment before the caret as typing to the worker. + // Declare before worker.onmessage, which may deliver textInput immediately. const inputFilter = new TextInput(hidden, { text: (text) => post({ type: "text", text }), backspace: () => { diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs index b139532c3..3bf1596ff 100644 --- a/web/tests/embed.test.mjs +++ b/web/tests/embed.test.mjs @@ -148,6 +148,7 @@ const workerMessages = []; let workerInstance; const { TextInput } = await import(pathToFileURL(path.join(dir, "web.js")).href); TextInput.prototype.clear = () => { filterClears++; }; +// This spy checks routing; text-input.test.js exercises the real filter and Backspace. TextInput.prototype.keydown = (ev) => { filteredKeys.push(ev.key); }; Canvas.prototype.transferControlToOffscreen = function () { return {}; }; globalThis.OffscreenCanvas = class {}; @@ -176,11 +177,11 @@ workerCanvas.dispatch("pointerdown", { pointerId: 1, pointerType: "mouse", butto assert.equal(filterClears, 2, "pointer input still clears the filter"); const hidden = document.body.children[0]; assert.equal(key(hidden).defaultPrevented, false, "the worker hidden input is recognized without asking main-thread wasm"); -for (const key of ["a", "Backspace"]) { - hidden.dispatch("keydown", { key, preventDefault() {} }); - hidden.dispatch("keyup", { key }); +for (const pressedKey of ["a", "Backspace"]) { + hidden.dispatch("keydown", { key: pressedKey, preventDefault() {} }); + hidden.dispatch("keyup", { key: pressedKey }); } -assert.deepEqual(filteredKeys, ["a", "Backspace"], "typing and backspace reach the filter after early text input"); +assert.deepEqual(filteredKeys, ["a", "Backspace"], "typing and backspace are routed to the filter spy after early text input"); assert.deepEqual(workerMessages.filter((m) => m.type === "key").map((m) => [m.kind, m.key]), [ [5, "a"], [6, "a"], [5, "Backspace"], [6, "Backspace"], ], "typing and backspace still reach the worker"); From c2a9f4851c6d8fa93e5cd3434fa9e8fa12f4a682 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:11:45 -0500 Subject: [PATCH 4/4] linux: work-633 (codex) Agent-Team: linux Worker: codex --- web/tests/embed.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs index 3bf1596ff..117776e37 100644 --- a/web/tests/embed.test.mjs +++ b/web/tests/embed.test.mjs @@ -144,7 +144,6 @@ console.log("embed.test: ok"); let earlyTextInputs = 0; let filterClears = 0; const filteredKeys = []; -const workerMessages = []; let workerInstance; const { TextInput } = await import(pathToFileURL(path.join(dir, "web.js")).href); TextInput.prototype.clear = () => { filterClears++; }; @@ -153,6 +152,7 @@ TextInput.prototype.keydown = (ev) => { filteredKeys.push(ev.key); }; Canvas.prototype.transferControlToOffscreen = function () { return {}; }; globalThis.OffscreenCanvas = class {}; globalThis.ResizeObserver = class { observe() {} }; +const workerMessages = []; globalThis.Worker = class { constructor() { workerInstance = this; } set onmessage(handler) {