diff --git a/.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md b/.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md new file mode 100644 index 0000000..aa615e1 --- /dev/null +++ b/.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md @@ -0,0 +1,204 @@ +--- +phase: 01-stabilize-video-pipeline +plan: 09 +subsystem: offscreen-recorder + background-sw + popup + manifest +tags: + - getDisplayMedia + - displaySurface + - cursor + - toolbar + - chrome.action.onClicked + - chrome.notifications + - chrome.runtime.onStartup + - badge-state-machine + - manifest + - popup-save-only + - D-15-display-surface +requires: + - 01-08 (Plan 01-08 SUMMARY + Tier-1 SW-bundle-import gate GREEN) +provides: + - REQ-video-ring-buffer:closure-via-D-15-display-surface + - operator-facing badge state machine (REC/OFF/ERROR) + recovery notifications + - getDisplayMedia constraint hint (displaySurface:'monitor', cursor:'always') + + post-grant enforcement (wrong-display-surface RECORDING_ERROR) + - toolbar-onClicked direct-to-picker flow (popup SAVE-only) + - onStartup notification inviting fresh recording on browser launch +affects: + - src/offscreen/recorder.ts (CaptureErrorCode union + classifyCaptureError + + getDisplayMedia call site + post-grant validation block) + - src/background/index.ts (badge state machine + 3 mode helpers + 3 new + listener registrations + RECORDING_ERROR onMessage branch + + initialize() setIdleMode call) + - src/popup/index.ts (SAVE-only — checkPermissions + requestPermissions + removed; popupState defaults to hasPermissions:true so SAVE is enabled) + - manifest.json (added 'notifications' permission) + - smoke.sh (SHARE_TARGET monitor mode + locale-fallback comment + + HTML stub instruction updates; T+/wall overlay preserved) +tech-stack: + added: + - chrome.action.onClicked (MV3) + - chrome.notifications.create / clear / onClicked + - chrome.runtime.onStartup + - MediaTrackConstraints displaySurface + cursor (Screen Capture spec) + patterns: + - 3-state badge state machine driven by setBadgeState helper + - setPopup('' ⇄ html) dance gating onClicked vs popup-open + - Constraint-hint-vs-enforcement gap: getDisplayMedia constraints are + HINTS per W3C spec; post-grant track.getSettings().displaySurface + check is the actual enforcement + - Notification id prefix namespace ('mokosh-startup-', 'mokosh-recovery-') + for spoofing mitigation (T-1-09-01) + - Defense-in-depth try/catch around all chrome.* listener registrations + so unit-test stubs that don't define every surface don't crash SW load +key-files: + created: + - tests/offscreen/display-surface-constraint.test.ts (4 tests) + - tests/background/toolbar-action.test.ts (5 tests inc. W-02 Test E) + - tests/background/badge-state-machine.test.ts (4 tests) + - tests/background/onstartup-notification.test.ts (4 tests) + modified: + - src/offscreen/recorder.ts (CaptureErrorCode + getDisplayMedia + validation) + - src/background/index.ts (~150 LOC of helpers + listeners + handler) + - src/popup/index.ts (~50 LOC removed, defaults flipped) + - manifest.json (1 permission added) + - smoke.sh (3 string literals + 14 comment lines) +decisions: + - displaySurface constraint is shipped with strict deep-equality test + pinning the EXACT constraints object {video:{displaySurface:'monitor', + cursor:'always'},audio:false} — a future refactor that drops either + constraint silently fails loudly. cursor:'always' was opportunistically + lifted from the Phase 5 deferral list per the I-03 fix. + - Post-grant validation is the actual enforcement (spec treats + displaySurface as a hint, not a hard constraint). Stream tracks are + stopped + mediaStream nulled + Error thrown — the existing catch + block routes it through classifyCaptureError + RECORDING_ERROR. + - setPopup '' ⇄ html dance chosen over removing default_popup entirely + (Option B per the plan interfaces): keeps the manifest stable + + lets the SW dynamically gate onClicked vs popup-open per state. + - Popup defaults flipped to hasPermissions:true / isRecording:true + because under the SAVE-only charter the popup only opens when + recording is already active (setPopup html path is set by + setRecordingMode/setErrorMode). + - All Plan 01-09 listener registrations wrapped in try/catch to + preserve the 6 pre-existing background tests (request-id-protocol + + port-lifecycle-continuous) whose chrome stubs predate + chrome.action/notifications/onStartup. Production MV3 always + provides these surfaces. + - smoke.sh SHARE_TARGET set to English 'Entire screen' with a 14-line + locale-fallback comment citing chromium/grit generated_resources.grd + as the authoritative source. Per W-04 fix the prior citation to + desktop_capture_device_uma_types.cc (UMA enums) was wrong and dropped. +metrics: + duration: ~35 minutes (executor agent) + completed: 2026-05-17 + task_count: 4 autonomous (Task 5 is operator-gated checkpoint, deferred) + files_modified: 9 (4 created + 5 modified) + net_loc: +1200 (~887 test LOC + ~310 src LOC + ~14 smoke comment lines) +--- + +# Phase 1 Plan 9: Toolbar-onClicked Direct-to-Picker + Display-Surface Monitor + Badge State Machine + Notifications + SAVE-only Popup + +Activation-cost reduction (3 clicks → 2) + footgun retirement (operator-picks-tab no longer reaches recording) + operator-visible recording state via badge + OS notifications inviting fresh starts and surfacing errors. + +## One-Liner + +`displaySurface:'monitor' + cursor:'always'` getDisplayMedia constraints with post-grant enforcement; `chrome.action.onClicked` toolbar-to-picker flow (popup SAVE-only); 3-state badge machine (REC/OFF/ERROR) + `chrome.runtime.onStartup` notification + `chrome.notifications.onClicked` recovery flow. + +## What Landed + +### Tests (17 new, all GREEN end-of-plan) + +| Test File | Tests | Contract Pinned | +| --- | --- | --- | +| `tests/offscreen/display-surface-constraint.test.ts` | 4 | getDisplayMedia exact constraints + non-monitor pick teardown + monitor over-fire guard + classifyCaptureError branch | +| `tests/background/toolbar-action.test.ts` | 5 (A-E) | onClicked registered + no-record→start + recording→no-double-start + setPopup state-machine + popup SAVE-only init contract (W-02 Test E) | +| `tests/background/badge-state-machine.test.ts` | 4 | REC palette + OFF palette (init) + ERROR palette + RECORDING_ERROR-triggered ERROR transition | +| `tests/background/onstartup-notification.test.ts` | 4 | onStartup registered + creates mokosh-startup- notification + onClicked clears + START_RECORDING + RECORDING_ERROR triggers mokosh-recovery- | + +### Source Changes + +- **`src/offscreen/recorder.ts`** — `CaptureErrorCode` union extended with `'wrong-display-surface'`; `classifyCaptureError` matches the prefix; `getDisplayMedia` call carries `{video:{displaySurface:'monitor',cursor:'always'},audio:false}` (typed via explicit widening cast — lib.dom.d.ts omits `cursor`); post-grant validation reads `track.getSettings().displaySurface`, on mismatch tears down stream + nulls mediaStream + throws `wrong-display-surface` Error which routes through the existing catch+RECORDING_ERROR pipeline. + +- **`src/background/index.ts`** — Added badge palette constants (SCREAMING_SNAKE), `setBadgeState(state: 'REC'|'OFF'|'ERROR')` helper (3-state machine), `setIdleMode`/`setRecordingMode`/`setErrorMode` mode helpers, `startVideoCapture` wires `setRecordingMode` on success and `setErrorMode` in catch, `chrome.action.onClicked` direct-to-picker listener, `chrome.runtime.onStartup` notification creator, `chrome.notifications.onClicked` mokosh-prefix-gated handler, RECORDING_ERROR onMessage branch with recovery notification, `initialize()` calls `setIdleMode` at boot. All registrations defensively wrapped in try/catch. + +- **`src/popup/index.ts`** — Removed `checkPermissions` + `requestPermissions` functions; `popupState` defaults to `hasPermissions:true, isRecording:true` under SAVE-only charter; `init()` no longer fires REQUEST_PERMISSIONS; empty-state copy points operator back to toolbar icon. + +- **`manifest.json`** — Added `"notifications"` to permissions array. + +- **`smoke.sh`** — `SHARE_TARGET="Entire screen"` (locale-English) + 14-line locale-fallback comment block citing Chromium grit generated_resources.grd as authoritative source for IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_SCREEN identifier (locale strings drift across Chrome versions); HTML title made distinct from screen-source string; `