diff --git a/system/public/aesthetic.computer/disks/nopaint.mjs b/system/public/aesthetic.computer/disks/nopaint.mjs index 6e507d411..3a3d6b14a 100644 --- a/system/public/aesthetic.computer/disks/nopaint.mjs +++ b/system/public/aesthetic.computer/disks/nopaint.mjs @@ -187,8 +187,7 @@ function transition(next) { } function interfaceLayout(screen) { - const barHeight = Math.max(96, Math.floor(screen.height * 0.18)); - const statusHeight = Math.max(22, Math.floor(barHeight * 0.22)); + const barHeight = Math.max(96, Math.floor(screen.height * 0.2)); const available = { x: 0, y: 0, w: screen.width, h: screen.height - barHeight }; const source = paintingResolution || { width: available.w, height: available.h }; const scale = Math.min(available.w / source.width, available.h / source.height); @@ -203,9 +202,9 @@ function interfaceLayout(screen) { bar: { x: 0, y: screen.height - barHeight, w: screen.width, h: barHeight }, status: { x: 0, - y: screen.height - statusHeight, + y: screen.height, w: screen.width, - h: statusHeight, + h: 0, }, scale, }; @@ -215,24 +214,23 @@ function positionButtons(screen) { // The recovered instrument keeps the decision pair together along the // bottom edge: No on the left, Paint larger on the right. They are the // architecture of the surface, not ordinary toolbar buttons. - const { bar, status } = interfaceLayout(screen); + const { bar } = interfaceLayout(screen); const gap = Math.max(4, Math.floor(screen.width * 0.006)); - const margin = Math.max(6, Math.floor(screen.width * 0.008)); - const available = screen.width - margin * 2 - gap; + const available = screen.width - gap; const noWidth = Math.floor(available * 0.38); const paintWidth = available - noWidth; - const buttonY = bar.y + margin; - const decisionHeight = status.y - buttonY - margin; + const buttonY = bar.y; + const decisionHeight = bar.h; const no = noButton.btn || noButton; const paint = paintButton.btn || paintButton; no.box ||= {}; paint.box ||= {}; Object.assign(no.box, { - x: margin, y: buttonY, + x: 0, y: buttonY, w: noWidth, h: decisionHeight, }); Object.assign(paint.box, { - x: margin + noWidth + gap, y: buttonY, + x: noWidth + gap, y: buttonY, w: paintWidth, h: decisionHeight, }); const done = doneButton?.btn || doneButton; @@ -240,14 +238,14 @@ function positionButtons(screen) { if (done) { done.box ||= {}; Object.assign(done.box, { - x: margin + noWidth + gap, y: buttonY, + x: noWidth + gap, y: buttonY, w: paintWidth, h: decisionHeight, }); } if (back) { back.box ||= {}; Object.assign(back.box, { - x: margin, y: buttonY, + x: 0, y: buttonY, w: noWidth, h: decisionHeight, }); } @@ -508,6 +506,7 @@ function paintingFingerprint(painting) { } function testSnapshot() { + if (testApi?.screen && noButton && paintButton) positionButtons(testApi.screen); const controlBox = (control) => { const box = control?.box || control?.btn?.box; return box ? { x: box.x, y: box.y, w: box.w, h: box.h } : null; diff --git a/system/public/aesthetic.computer/disks/prompt.mjs b/system/public/aesthetic.computer/disks/prompt.mjs index edc88eaa4..afa90b3e7 100644 --- a/system/public/aesthetic.computer/disks/prompt.mjs +++ b/system/public/aesthetic.computer/disks/prompt.mjs @@ -135,6 +135,7 @@ let progressTrick; // A faux growth period on the progress bar. let cachedGizmo; // Reference to gizmo for use in act() function let progressPhase = ""; // Current phase of upload (e.g., "ZIPPING", "UPLOADING IMAGE") let progressPercentage = 0; // 0-100 +let paintingCompletionBusy = false; // 📦 Pack progress state let packProgress = null; // { timeline, startTime, code } or null @@ -1099,6 +1100,7 @@ async function boot({ net: { socket }, vscode, }) { + paintingCompletionBusy = false; promptSend = send; promptNeedsPaint = needsPaint; cachedGizmo = gizmo; // Cache gizmo for use in act() function @@ -2477,6 +2479,8 @@ async function halt($, text) { makeFlash($); return true; } else if (slug === "painting:done" || slug === "yes!" || slug === "done") { + paintingCompletionBusy = true; + send({ type: "keyboard:lock" }); let destination = params[0] || "upload"; // or "upload" if (destination === "u" || slug === "yes!") destination = "upload"; // ^ "yes!" is always an upload. @@ -2587,6 +2591,7 @@ async function halt($, text) { progressBar = -1; progressPhase = ""; progressPercentage = 0; + paintingCompletionBusy = false; makeFlash($); return true; } @@ -7853,6 +7858,13 @@ function act({ notice, ui, }) { + // Done is a transaction, not a background task. Prompt remains visible as + // its progress surface but cannot accept edits or commands until upload + // yields painting#CODE or reports a recoverable failure. + if (paintingCompletionBusy) { + send({ type: "keyboard:lock" }); + return; + } // 🎞️ Rolodex: a predominantly-vertical drag on the focused prompt // scrubs command history pixel-by-pixel and stays where you let go // (the snap). Only engages past a threshold so taps / horizontal diff --git a/tests/browser/ac-harness.mjs b/tests/browser/ac-harness.mjs index 1bf0d8647..005e9611f 100644 --- a/tests/browser/ac-harness.mjs +++ b/tests/browser/ac-harness.mjs @@ -87,7 +87,10 @@ export class ACSession { setTimeout(() => clearInterval(timer), 5000); } }); - s.page.on("pageerror", (e) => console.warn(" ⚠️ pageerror:", e.message)); + s.page.on("pageerror", (e) => console.warn( + " ⚠️ pageerror:", + e?.message || String(e || "unknown browser error"), + )); s.downloadDir = join(CONFIG.shotDir, "downloads", String(Date.now())); mkdirSync(s.downloadDir, { recursive: true }); await s.page._client().send("Page.setDownloadBehavior", { diff --git a/tests/browser/nopaint-journey.test.mjs b/tests/browser/nopaint-journey.test.mjs index 5704502a1..cc2077447 100644 --- a/tests/browser/nopaint-journey.test.mjs +++ b/tests/browser/nopaint-journey.test.mjs @@ -81,6 +81,11 @@ try { "all controls share the below-painting control bar", ); expect(state?.controls?.no?.y === state?.controls?.paint?.y, "No and Paint align in one bottom row"); + expect( + state?.controls?.no?.y + state?.controls?.no?.h === state?.layout?.screenResolution?.height && + state?.controls?.paint?.y + state?.controls?.paint?.h === state?.layout?.screenResolution?.height, + "No and Paint are flush with the bottom edge", + ); expect( state?.layout?.controlBar?.x === 0 && state?.layout?.controlBar?.w >= @@ -131,8 +136,8 @@ try { `${viewport.label}: controls stay inside bar`); expect(no.x + no.w <= paint.x && paint.x + paint.w <= bar.x + bar.w, `${viewport.label}: No and Paint do not overlap`); - expect(no.y + no.h <= modeline.y && paint.y + paint.h <= modeline.y, - `${viewport.label}: modeline stays below No and Paint`); + expect(no.y + no.h === screenResolution.height && paint.y + paint.h === screenResolution.height, + `${viewport.label}: controls remain flush with the bottom edge`); } // Synthetic viewport probes may exceed the filming display. Return to the // harness's screen-safe size before taking any more native Frame receipts.