From b9eeeeb386cbeac11ca2917ba236e34e19221484 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 17 May 2026 16:49:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(01-09):=20GREEN=20=E2=80=94=20Bug=20B=20ro?= =?UTF-8?q?ute=20user-stopped-sharing=20=E2=86=92=20IDLE;=20other=20codes?= =?UTF-8?q?=20=E2=86=92=20ERROR=20(preserved)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Patches the RECORDING_ERROR onMessage handler in src/background/index.ts (lines 725-744 pre-patch) with conditional routing on the incoming `message.error` payload: - 'user-stopped-sharing' → setIdleMode() (popup empties; badge OFF; isRecording flipped to false). Recovery notification suppressed — the operator stopped deliberately, surfacing one would be UX noise. The offscreen recorder's onUserStoppedSharing has already cleared the buffer (src/offscreen/recorder.ts:457 resetBuffer), so IDLE is the correct landing state. - all other codes → setErrorMode() + recovery notification, preserving the existing operator-facing surface for genuine capture failures (codec-unsupported, wrong-display-surface, capture-failed, etc.). Closes the operator-lockout regression observed in Plan 01-09 Task 5 empirical UAT: after Chrome's "Stop sharing" banner click, the badge stayed yellow and the popup pinned to SAVE-only, gating chrome.action.onClicked behind the popup forever. Operator had no restart path. With IDLE routing, the popup empties and the toolbar click fires startVideoCapture as designed. Tests: 83/83 GREEN (was 81; +2 from Tests E+F). tsc clean. Build exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/background/index.ts | 62 +++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/src/background/index.ts b/src/background/index.ts index d6966f5..2c9ea8e 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -725,21 +725,55 @@ chrome.runtime.onMessage.addListener((message: Message, sender, sendResponse) => case 'RECORDING_ERROR': // Plan 01-09 — the offscreen recorder broadcasts this on capture // failure (codec missing, picker cancelled, wrong-display-surface, - // mid-record stream end, etc.). Surface to the operator via the - // badge + a recovery notification. + // mid-record stream end, etc.) AND on operator-initiated stop-sharing + // (src/offscreen/recorder.ts: onUserStoppedSharing emits + // RECORDING_ERROR{error:'user-stopped-sharing'} after resetBuffer + + // track release). + // + // Bug B (debug 01-09-recovery-flow) conditional routing: the + // operator-initiated stop is NOT an error condition, it is a + // deliberate end-of-session signal. Routing it through setErrorMode + // would (a) leave the popup pinned to src/popup/index.html (SAVE-only) + // so chrome.action.onClicked cannot re-fire — the popup wins the + // toolbar click forever, and (b) paint the badge yellow as if a + // capture failure occurred. Both lock the operator out of restart. + // Route 'user-stopped-sharing' through setIdleMode instead: popup + // empties (re-enabling onClicked-driven restart), badge returns to + // OFF (resetBuffer has already cleared the offscreen buffer so SAVE + // mode would be meaningless). No recovery notification: the operator + // performed the stop deliberately; surfacing a notification would + // be UX noise. + // All other error codes preserve the original setErrorMode + recovery + // notification routing (defensive fallback for genuine capture + // failures — codec-unsupported, wrong-display-surface, capture-failed, + // permission-denied, empty-video-buffer, unknown). logger.warn('RECORDING_ERROR received:', message); - setErrorMode(); - try { - const recoveryId = NOTIFICATION_RECOVERY_PREFIX + Date.now(); - chrome.notifications.create(recoveryId, { - type: 'basic', - iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH), - title: 'Mokosh stopped', - message: 'Recording stopped. Click here to start a new session.', - priority: 1, - }); - } catch (e) { - logger.warn('Recovery notification create failed:', e); + { + // Narrow `message` to read the optional `error` payload. The + // canonical Message interface (src/shared/types.ts) does not + // typify it (Message has type/data/tabId only); the offscreen + // recorder emits the extra `error` field as part of the + // RECORDING_ERROR wire shape — read it via a minimal cast that + // keeps us off `as any`. + const errorCode = (message as unknown as { error?: unknown }).error; + if (errorCode === 'user-stopped-sharing') { + isRecording = false; + setIdleMode(); + } else { + setErrorMode(); + try { + const recoveryId = NOTIFICATION_RECOVERY_PREFIX + Date.now(); + chrome.notifications.create(recoveryId, { + type: 'basic', + iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH), + title: 'Mokosh stopped', + message: 'Recording stopped. Click here to start a new session.', + priority: 1, + }); + } catch (e) { + logger.warn('Recovery notification create failed:', e); + } + } } return false;