From 5a15c0c795b0475b044e7f702121a466abab8ece Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Fri, 11 Sep 2026 19:35:29 +0200 Subject: [PATCH 1/5] fix: shrink QR to fit smaller terminals Use EC level 'L' (one QR version smaller for our URL length) and a 1-module quiet zone, saving ~6 rows. The OpenCode instructions dialog is not scrollable, so an oversized QR clips the modal on small screens. --- src/plugin/device-flow.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugin/device-flow.ts b/src/plugin/device-flow.ts index 22219a6..19f40a8 100644 --- a/src/plugin/device-flow.ts +++ b/src/plugin/device-flow.ts @@ -187,7 +187,7 @@ function formatPollError(errorData: TokenErrorResponse): string { return `Device flow failed: ${errorData.error} — ${description}`; } -const QR_QUIET_ZONE_MODULES = 2; +const QR_QUIET_ZONE_MODULES = 1; /** * Single token poll request. Returns undefined on transport errors or @@ -238,7 +238,11 @@ async function fetchTokenPollBody( * Light blocks on the terminal's dark background — scannable on dark themes. */ async function generateTerminalQrCode(data: string): Promise { - const code = QRCode.create(data, { errorCorrectionLevel: 'M' }); + // Error correction 'L' keeps the matrix one version smaller than 'M' for + // our URL length, saving several terminal rows — the instructions dialog + // is not scrollable in the OpenCode TUI, so height matters more than + // damage tolerance for a QR displayed on a clean screen. + const code = QRCode.create(data, { errorCorrectionLevel: 'L' }); const size = code.modules.size; const rows: string[] = []; From 09bcecc018df86009a4b0af0a81e2b782d4104db Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Fri, 11 Sep 2026 19:39:09 +0200 Subject: [PATCH 2/5] fix: render QR as quadrant blocks to halve modal height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2x2 modules per character (▘▝▖▗-style glyphs) instead of half-blocks: the v4/L matrix now takes ~19 rows and ~19 cols instead of ~19 rows and ~35 cols... i.e. half the width and half the height, so the centered dialog fits small terminals. Verified decodable to the exact verification_uri_complete with a ZXing round-trip. --- src/plugin/device-flow.ts | 81 +++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/plugin/device-flow.ts b/src/plugin/device-flow.ts index 19f40a8..a8c8d0b 100644 --- a/src/plugin/device-flow.ts +++ b/src/plugin/device-flow.ts @@ -187,7 +187,30 @@ function formatPollError(errorData: TokenErrorResponse): string { return `Device flow failed: ${errorData.error} — ${description}`; } -const QR_QUIET_ZONE_MODULES = 1; +const QR_QUIET_ZONE_MODULES = 2; + +/** + * Quadrant block glyphs indexed by a 2x2 module pattern: + * bit 0 = top-left, bit 1 = top-right, bit 2 = bottom-left, bit 3 = bottom-right. + */ +const QUADRANT_GLYPHS = [ + ' ', + '▘', + '▝', + '▀', + '▖', + '▌', + '▞', + '▛', + '▗', + '▚', + '▐', + '▜', + '▄', + '▙', + '▟', + '█', +] as const; /** * Single token poll request. Returns undefined on transport errors or @@ -231,49 +254,43 @@ async function fetchTokenPollBody( } /** - * Renders the QR matrix manually as half-block pairs: one character - * covers two vertical modules using ▀/▄/█/space. Full-width spaces and - * double characters get collapsed by the OpenCode TUI, so this - * half-block encoding is the only reliable rendering there. + * Renders the QR matrix as quadrant blocks: one character covers a 2x2 + * module area using ▘▝▖▗-style glyphs, halving both width and height + * compared to half-block rendering. The instructions dialog is not + * scrollable in the OpenCode TUI and is vertically centered, so every + * saved row counts on small terminals. * Light blocks on the terminal's dark background — scannable on dark themes. */ async function generateTerminalQrCode(data: string): Promise { // Error correction 'L' keeps the matrix one version smaller than 'M' for - // our URL length, saving several terminal rows — the instructions dialog - // is not scrollable in the OpenCode TUI, so height matters more than - // damage tolerance for a QR displayed on a clean screen. + // our URL length — damage tolerance matters little on a clean screen. const code = QRCode.create(data, { errorCorrectionLevel: 'L' }); const size = code.modules.size; + const total = size + QR_QUIET_ZONE_MODULES * 2; + + const moduleAt = (row: number, col: number): number => { + const qrRow = row - QR_QUIET_ZONE_MODULES; + const qrCol = col - QR_QUIET_ZONE_MODULES; + if (qrRow < 0 || qrRow >= size || qrCol < 0 || qrCol >= size) { + return 0; + } + return code.modules.get(qrRow, qrCol) === 1 ? 1 : 0; + }; const rows: string[] = []; - const rowWidth = size + QR_QUIET_ZONE_MODULES * 2; - const quietRow = ' '.repeat(rowWidth); - - rows.length = QR_QUIET_ZONE_MODULES; - rows.fill(quietRow); - - for (let r = 0; r < size; r += 2) { - let row = ' '.repeat(QR_QUIET_ZONE_MODULES); - for (let c = 0; c < size; c += 1) { - const top = code.modules.get(r, c) === 1; - const bottom = r + 1 < size && code.modules.get(r + 1, c) === 1; - if (top && bottom) { - row += '█'; - } else if (top) { - row += '▀'; - } else if (bottom) { - row += '▄'; - } else { - row += ' '; - } + for (let r = 0; r < total; r += 2) { + let row = ''; + for (let c = 0; c < total; c += 2) { + const pattern = + moduleAt(r, c) | + (moduleAt(r, c + 1) << 1) | + (moduleAt(r + 1, c) << 2) | + (moduleAt(r + 1, c + 1) << 3); + row += QUADRANT_GLYPHS[pattern]; } rows.push(row); } - for (let index = 0; index < QR_QUIET_ZONE_MODULES; index += 1) { - rows.push(quietRow); - } - return rows.join('\n'); } From fcceb25380743c49ca61a98c9763473e14c8c639 Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Fri, 11 Sep 2026 19:40:23 +0200 Subject: [PATCH 3/5] fix: revert to half-block QR rendering Quadrant glyphs assume square terminal cells; cells are ~1:2 (w:h), so the QR rendered twice as tall as wide. Half-blocks give square pixels (1 module = 1 char wide, half a char tall). Keeps EC 'L' and a 2-module quiet zone. --- src/plugin/device-flow.ts | 52 +++++++++++++-------------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/src/plugin/device-flow.ts b/src/plugin/device-flow.ts index a8c8d0b..271354e 100644 --- a/src/plugin/device-flow.ts +++ b/src/plugin/device-flow.ts @@ -189,29 +189,6 @@ function formatPollError(errorData: TokenErrorResponse): string { const QR_QUIET_ZONE_MODULES = 2; -/** - * Quadrant block glyphs indexed by a 2x2 module pattern: - * bit 0 = top-left, bit 1 = top-right, bit 2 = bottom-left, bit 3 = bottom-right. - */ -const QUADRANT_GLYPHS = [ - ' ', - '▘', - '▝', - '▀', - '▖', - '▌', - '▞', - '▛', - '▗', - '▚', - '▐', - '▜', - '▄', - '▙', - '▟', - '█', -] as const; - /** * Single token poll request. Returns undefined on transport errors or * non-JSON bodies (e.g. a 502 HTML page from the gateway in front of @@ -254,11 +231,11 @@ async function fetchTokenPollBody( } /** - * Renders the QR matrix as quadrant blocks: one character covers a 2x2 - * module area using ▘▝▖▗-style glyphs, halving both width and height - * compared to half-block rendering. The instructions dialog is not - * scrollable in the OpenCode TUI and is vertically centered, so every - * saved row counts on small terminals. + * Renders the QR matrix as half-block pairs: one character covers two + * vertical modules using ▀/▄/█/space. Terminal cells are ~1:2 (w:h), + * so one module = one char wide, half a char tall — i.e. square pixels. + * (Quadrant ▘▝▖▗ rendering halves the height further but assumes square + * cells, producing a stretched, unreliable-to-scan code.) * Light blocks on the terminal's dark background — scannable on dark themes. */ async function generateTerminalQrCode(data: string): Promise { @@ -280,13 +257,18 @@ async function generateTerminalQrCode(data: string): Promise { const rows: string[] = []; for (let r = 0; r < total; r += 2) { let row = ''; - for (let c = 0; c < total; c += 2) { - const pattern = - moduleAt(r, c) | - (moduleAt(r, c + 1) << 1) | - (moduleAt(r + 1, c) << 2) | - (moduleAt(r + 1, c + 1) << 3); - row += QUADRANT_GLYPHS[pattern]; + for (let c = 0; c < total; c += 1) { + const top = moduleAt(r, c) === 1; + const bottom = moduleAt(r + 1, c) === 1; + if (top && bottom) { + row += '█'; + } else if (top) { + row += '▀'; + } else if (bottom) { + row += '▄'; + } else { + row += ' '; + } } rows.push(row); } From 8a69a1e3695ac168d7aaa9dc88d483568136e1b4 Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Fri, 11 Sep 2026 19:43:22 +0200 Subject: [PATCH 4/5] fix: tighten instructions layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the '─ or ─' divider and merge the validity note into the fallback line, saving 4 rows of vertical space. --- src/plugin/device-flow.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/plugin/device-flow.ts b/src/plugin/device-flow.ts index 271354e..d4f9609 100644 --- a/src/plugin/device-flow.ts +++ b/src/plugin/device-flow.ts @@ -95,20 +95,12 @@ async function buildInstructions( const qrCode = await generateTerminalQrCode(verificationUri); const validMinutes = Math.round(deviceInfo.expires_in / 60); - const dividerLabel = ' or '; - const dividerDashCount = Math.floor((DIALOG_CONTENT_WIDTH - dividerLabel.length) / 2); - const divider = `${'─'.repeat(dividerDashCount)}${dividerLabel}${'─'.repeat(dividerDashCount)}`; - const lines = [ 'Scan with your phone:', '', ...qrCode.split('\n').map((line) => centerLine(line)), '', - divider, - '', - 'Or open the link below — the code is included.', - '', - `Valid for ${validMinutes} minutes.`, + `Or open the link below — the code is included (valid for ${validMinutes} minutes).`, ]; return lines.join('\n'); } From 8c644a10627f2e9c5611ed20371db8b3cdd2ade8 Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Sat, 12 Sep 2026 08:57:22 +0200 Subject: [PATCH 5/5] fix: refer to the link above (rendered at the top of the dialog) --- src/plugin/device-flow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin/device-flow.ts b/src/plugin/device-flow.ts index d4f9609..1a1f6ff 100644 --- a/src/plugin/device-flow.ts +++ b/src/plugin/device-flow.ts @@ -100,7 +100,7 @@ async function buildInstructions( '', ...qrCode.split('\n').map((line) => centerLine(line)), '', - `Or open the link below — the code is included (valid for ${validMinutes} minutes).`, + `Or open the link above — the code is included (valid for ${validMinutes} minutes).`, ]; return lines.join('\n'); }