14 pinning tests lock downloadAppUpdate's security invariants (redirect
re-validation per hop, checksum refusal paths, SHA-256 verify + cleanup,
truncation, idle-timeout, cancel in both phases, progress) with a
scripted net.request fake; then the nested streaming block is extracted
verbatim into streamInstallerToFile. downloadAppUpdate is now a linear
pre-flight; B6 cancel-slot ownership stays with its module state via
onCancelReady/onSettled. Pins green before and after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI23: cookie sign-in + PO-token windows open with the app's resolved
theme background (passed from the IPC layer), no more white flash.
W12: multi-resolution fallback tray icon (16/24/32/48 via
addRepresentation dataURL, embedded in trayFallbackIcons.ts); decode +
scale factors verified in an Electron probe.
W15: hardwareAcceleration setting (default false = software rendering);
index.ts gates disableHardwareAcceleration on it after the portable
redirect; Appearance card switch with restart note; boot path verified.
L131: resolved by-design — clearing the queue acknowledges failures;
flashFrame (W10) covers attention, Diagnostics persists the record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UX7: 11 cards grouped into five sections (Downloads, Appearance,
Network & accounts, Advanced, App & maintenance) with section headings
and a jump nav under the search box; search hides empty sections and
the nav; heading hierarchy corrected (h1 page, h2 section, h3 card).
UX8: post-processing card co-located under the Downloads card and
renamed 'Post-processing' with a defaults-vs-advanced hint.
UX23: closed as stale duplicate of the already-fixed L71 (editable,
validated, reconciled folder inputs) — verified in the preview probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI3: remaining list-row/control paddings on the SPACE scale (History row,
Downloads summary, Library head/detail, Terminal gate/log, template rows).
UI21: active-nav rail now shows collapsed too (moved into navItemActive).
UX18: stable 'Quality' label + brand '• fetched' tag instead of the
label/control morph after Fetch.
L109/UX26: skeleton lines for the About yt-dlp/ffmpeg boot load; skeleton
cards in Settings until the store's loaded flag flips.
UI12/UI16: deferred eyeball deltas verified via Electron-probe screenshots
(light+dark, collapsed sidebar, fetch flow); UI16 convention codified in
ui/tokens.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L161: <main> is now a non-scrolling flex slot; each screen owns its scroll
via shared shells in ui/Screen.tsx — 'page' (Library/Settings/empty History
scroll the shell) and 'fill' (Downloads/Terminal/History scroll their own
list/log, shell scrolls only as short-window fallback). height:100%
couplings replaced with flexGrow+minHeight:0; list regions floored at
160px so they can't collapse on short windows.
L104: HistoryView renders through VirtualList; Ctrl+A re-homed onto a
wrapper around the scroller, per-row Delete unchanged on rows.
Verified with an Electron probe against the vite preview at 1100x720 and
800x520 (probe metrics + screenshots): one active scroller per screen,
main/body never scroll, floors hold. Adds .claude/launch.json for the
preview server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prettier's LF default fought git's CRLF checkout on Windows, failing
format:check on every file with no real diff. 'auto' keeps whatever the
checkout uses; content style is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L142 (+CC14): keyed latest-wins reconciler (lib/reconcile.ts, unit-tested)
applies main's authoritative IPC returns in the history/templates/sources
stores, extending the M34 settings pattern; loadSources shares the key so
a slow list read can't clobber a newer mutation result.
L93 (+CC13): settings cards, TerminalView, and LibraryView now reach IPC
only through colocated view-model hooks (useAboutCard/useBackupCard/
useCookiesCard/useSoftwareUpdateCard/useNetworkCard/useTerminalRun) or
store actions (openHighContrastSettings, scheduled-sync on sources) —
no window.api left in components.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two small self-contained features from the audit's deferred list:
- L51: the Task Scheduler daily sync is no longer pinned to 09:00. New
Settings.syncTime (24h HH:MM) with a native time input beside the Library
"Daily sync" switch — persists on change, re-registers the task on blur
(schtasks /Create /F overwrite is the time-change mechanism). The value
reaches the schtasks /ST argv, so a shared isValidSyncTime guards it at
both the settings write and in schedule.ts (unit-tested incl.
injection-shaped input). Live-verified against the machine's real task.
- L64: aria2c connection count (-x/-s) is user-tunable. New
Settings.aria2cConnections (1-16, aria2c's own ceiling) exposed as a
SpinButton in Settings -> Network while the aria2c toggle is on.
ARIA2C_ARGS became the pure aria2cArgs(n?) with a defensive clamp;
threaded through AccessOptions. The -k 1M split floor stays fixed — a
free-text field would bypass the custom-command consent gate.
typecheck + 309 tests + eslint + production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The watched live session for the six deferred lifecycle items, each verified
against real yt-dlp/Electron behavior on Windows:
- R4: cancel now deletes the download's orphaned partials. The engine captures
the final output path via a new `--print before_dl:dest|%(filename)s`
(empirically %(filepath)s is still 'NA' that early) and the close handler
sweeps only unambiguous intermediates (.part / .part-Frag* / .ytdl /
<stem>.fNNN.<ext>) via the pure, unit-tested lib/partials.ts — never a bare
<stem>.<ext> or another download's files. Live-verified with decoys.
- L65: verified-acceptable, no code change — a taskkill /F pause leaves the
.part byte-intact and yt-dlp --continue resumes at the exact byte offset.
- L139: spawn↔self-update interlock (new ytdlpLock.ts). The updater refuses
while any yt-dlp spawn is live; download/terminal IPC handlers hold (await)
behind an in-progress exe rewrite instead of failing.
- B2: source indexing is cancellable — AbortSignal through the probe walk
(kills the in-flight child, live-verified via tasklist), sources:index-cancel
IPC, and a Cancel button in the Library add-source row.
- B6: app-update download is cancellable — app:update-cancel routes through
the existing finish() teardown (abort + destroy + unlink, live-verified on
Electron net.request); Cancel button under the update progress bar. The
checksum phase is cancelable too.
- L143: closing the window mid-download (non-tray mode) now offers a native
tray-independent "Quit anyway / Keep downloading" dialog, replacing the
passive "use the tray to quit" notification.
Peer-review pass caught and fixed: a non-reentrant update lock (concurrent
begin clobbered the settle promise), a dead Cancel window during the B6
checksum fetch + a canceller slot-ownership bug (L140 shape), and a
persist-after-cancel hole in the index walk.
typecheck + 304 tests + eslint + production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconciles two lines of history that both descended from the same
feat/tray-background-clipboard branch (background running, clipboard-link
detection, updater auth): main picked it up as a single squash commit
(PR #7, d112bc4), while the audit chain (p1-docs..batch-15) branched off
that feature branch's pre-squash tip and continued with its own granular
history, including the H1 decomposition of DownloadBar.tsx/SettingsView.tsx.
12 files conflicted, all resolved by verifying the audit chain already
carries PR #7's functionality (confirmed per-file against the original
d112bc4 diff) and keeping the chain's newer/relocated version:
- shared/ipc.ts, main/settings.ts: DEFAULT_SETTINGS single-sourced (C1)
- main/download.ts, main/tray.ts: trivial, chain is a superset
- main/index.ts: ipcMain.handle wiring extracted to main/ipc.ts (L2);
removed a silent duplicate notifyBackgroundOnce that 3-way merge
produced with no conflict markers
- renderer/main.tsx, store/settings.ts: mock/fallback settings
single-sourced (C1/mockApi.ts)
- LibraryView.tsx: old inline clipboard-suggestion banner replaced by
the shared LinkSuggestion primitive (Batch 9/UI19); removed dead
icon imports + styles left behind by the auto-merge
- DownloadBar.tsx, SettingsView.tsx: taken wholesale from the chain
(H1-decomposed shells) after confirming their extracted hook/cards
already contain PR #7's additions (useClipboardLink.offer(),
launchAtStartup, updateToken)
- useClipboardLink.ts, test/clipboardLink.test.ts (add/add): chain's
version is a strict refinement of the same code (L40/L138/L145)
Verified post-merge: typecheck (node+web), 279 tests, eslint, and the
production build all pass; touched files are prettier-clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Self-review of the Batches 9-15 changes surfaced two style inconsistencies (no
behavior change):
- Toaster.tsx: combine the icon + tone classes with mergeClasses instead of a
template-literal string, matching how the rest of the codebase composes Griffel
classes.
- App.tsx: move the PERF8 React.lazy() view declarations below the import block
(they were interleaved between import statements).
Verified: typecheck (node+web) + 279 tests + eslint + production build green
(code-split chunks intact); touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This is the batch the run reserves for a WATCHED LIVE SESSION, so only the safe,
non-file-deleting item is implemented here; the rest are staged with per-item
live-verify rationale in CODE-AUDIT.md.
Landed:
- L157: removeSource cancels its in-flight downloads. removeSource emits a
`sourceRemoved` event on the coordinator bus (C2); the downloads store cancels
every active (downloading/queued) item whose mediaItemId starts with
`${sourceId}:` — MediaItem ids are `${sourceId}:${videoId}` (indexerCore), so the
prefix reliably identifies the source's downloads. Reuses the proven cancel()
path (process kill + pump). Type/lint/test-green; process-kill worth a live confirm.
Resolved by prior work:
- UX11: M4 persists the queue, so a scheduled item fires on next launch once due —
no longer "never fires if quit".
- UX20: L68 resets notifiedBackground on window show, so every close-to-tray
re-fires the "still running in the tray" notice, not just the first.
Deferred to the watched live pass (each needs a real download to verify):
R4 (delete .part on cancel — file-deletion data-safety), L65 (graceful Ctrl-C
pause), L139 (spawn↔auto-update interlock — timing race), B2 (index AbortSignal +
Cancel), B6 (app-update cancel — ties to CL4's deferred updater), L143 (in-window
quit-anyway).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PERF8: code-split the non-default tabs. Library/History/Terminal/Settings are
React.lazy chunks behind a Suspense; Downloads (the launch tab) stays eager. The
production build now emits separate SettingsView (~90kB), LibraryView (~29kB),
HistoryView (~16kB), TerminalView (~8kB) chunks — ~140kB out of the initial
bundle. The stores these views use are imported eagerly elsewhere, so lazy-
loading the views never delays store startup/persistence (per the C2 note).
- L128: gate the three preview seed arrays (downloadSeed / sources seeds / history
seed) on `import.meta.env.DEV && PREVIEW`. In the packaged build DEV is
statically false, so Rollup constant-folds and dead-code-eliminates the seed
literals; the browser dev preview (DEV true) still gets them.
- SR4: clipboardWatch now defaults false for new installs (privacy — no clipboard
read on focus until opted in). Existing installs keep their saved value
(electron-store fills only missing keys); the onboarding tip points to the
setting. NOTE: this flips a signature convenience off-by-default for new users —
flagged for the maintainer, trivially reversible.
Deferred with rationale (CODE-AUDIT.md): PERF3 (core-queue micro-opt on a bounded
list, risky unattended), PERF6/L162 (bounded by virtualization + conflicts with
PERF5's hoisted renderItem), SR5 (MP3 = deliberate compatibility default; the label
bug was M18), W12 (fallback-of-a-fallback icon), W15 (GPU software-rendering
workaround — external driver dependency), L104 (History virtualization needs the
L161 layout rework + live verification; bounded by the 500-cap).
Verified: typecheck (node+web) + 279 tests + eslint + production build green
(code-split chunks confirmed in output); touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contained fixes:
- L158: drag-highlight flicker. useDownloadBar tracks a dragDepth counter —
onDragEnter increments, onDragLeave decrements, the highlight clears only when
depth returns to 0 (the drag truly left the card). onDragOver just
preventDefaults; onDrop resets. No more blink when crossing child elements.
- L92: preload method names aligned with main — markSourceItemDownloaded →
setMediaItemDownloaded, syncSources → syncWatchedSources (callers + mock
updated; compiler-enforced via the derived Api type).
- L149: the three-sentence "Keep running in the tray" hint is now one line.
- L171: one MOCK_CURRENT_VERSION backs both the preview mock's getAppVersion and
checkForAppUpdate.currentVersion, so they agree.
- L5 (representative): the About card's repeated monospace inline styles → shared
mono/monoBlock/monoPre classes; convention set (makeStyles for static, inline
style only for data-driven values).
Resolved by verification/convention (CODE-AUDIT.md): L21 (moot after the H1
SettingsView decomposition), L31 (dual theme switcher is width-driven), L130
(lowercase fragments vs shown-raw sentences), L152 (search sizes are role-
appropriate), UI22 (brand/entity/section icon-emphasis set now defined).
Deferred with rationale: L51/L64 (features), L93 (CC13 view-model pass), L104
(moved to Batch 14 perf), L109/UI21 (visual), L161/UI23 (layout/live-verify).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- L118: decouple URL-field focus. The DownloadBar registers a focuser (its own
input ref) in the nav store; App calls useNav.getState().focusUrlField() on
launch / "New download" instead of document.getElementById('aerofetch-url').
The hardcoded id is removed — App no longer reaches into DownloadBar's markup.
- L155: the accent swatches are now a real role="radiogroup" with aria-checked
role="radio" swatches, roving tabindex, ←/→/↑/↓/Home/End arrow-key navigation,
and the shared focus ring — matching the app's SegmentedControl radio pattern.
Redundant "(selected)" aria-label text dropped (aria-checked conveys it).
- UX21: keyboard-shortcut discoverability — an Onboarding "Press Ctrl+K anytime"
tip, and the command palette's Settings action surfaces its "Ctrl ," shortcut.
- UX22 / L67: onboarding is revisitable — a "Show welcome tips again" button in
Settings → About re-opens the welcome screen.
Verified & documented (no code needed): UX17 (Fetch/Paste already have aria-label
+ focus-reachable Hint tooltips — a11y met; a permanent visible label is a compact-
layout choice), UI24 (context menus "none by design" — row actions are all visible
inline buttons + keyboard, and Fluent's Menu is portal-based, which the app avoids
for the dev-GPU flicker reason).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new in-app toast surface is the batch's spine, making the silent-failure and
global-status gaps fixable:
- Toast infra: store/toasts.ts (useToasts + toast(); auto-dismiss) + ui/Toaster.tsx
(non-portal bottom-center stack, avoids the Fluent-portal GPU flicker). Unit-
tested (test/toasts.test.ts). Mounted at the app root beside LiveRegion.
- UX6: shared renderer reveal.ts revealFile('open'|'folder') awaits the IPC result
and toasts the main-process reason instead of silently no-oping on a moved/typed-
out file. safeShowInFolder upgraded to return an error string like safeOpenPath
(preload/mock/Api types updated).
- UX9 / UI25: a Fluent CounterBadge on the sidebar Downloads nav item shows the
active (downloading + queued) count from any tab, via a count-only App selector
that doesn't re-render on progress ticks.
- UX15: cancelAll store action + a "Cancel all (N)" header button.
- UX24: the disabled "Check watched" button uses disabledFocusable + a Hint that
explains it needs a watched source.
- UX12: the Terminal gate gains an inline "Enable custom commands" button, so it's
no longer a dead-end pointing at another screen.
- L144: the DownloadBar duplicate guard now also checks history — a URL downloaded
earlier warns "Already downloaded: …" (dupInHistory) vs "Already in your queue: …".
Deferred with rationale (CODE-AUDIT.md): UX7/UX8 (Settings IA redesign — visual),
UX18/UX26 (subjective/visual polish), L131 (arguably by-design, needs live taskbar),
L142 (history/templates/sources IPC-return reconciliation — a focused own PR).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contained consistency consolidations; the sweeping ones are deferred with
their standard documented (they're the audit's own post-1.0 refactors, and
several would be reckless to do unattended).
Landed:
- src/main/lib/lineBuffer.ts (createLineBuffer): one newline line-splitter for
streamed child-process output, replacing the duplicated buffer loop in
download.ts and terminal.ts (CC3/CC4/SIMP5). Pure + unit-tested
(test/lineBuffer.test.ts, 7 cases: partial lines, CRLF, empty lines, flush,
multi-line chunks). download.ts keeps byte-identical marker parsing and no
close-flush (yt-dlp template lines are always newline-terminated).
- ytdlp.ts: getYtdlpVersion/updateYtdlp now run stderr through cleanError, the
same `cleanError(r.stderr) || r.error?.message || ''` idiom already used by
download/probe/indexer (CC6) — a failed check shows the trailing error line.
- CL6 resolved: the one real redundancy was L15; clearDir/openFile/showInFolder
are deliberate view-model wrappers (CC13), left as-is.
Deferred with documented standard + rationale (CODE-AUDIT.md): CC1 (breaking
persisted-key rename → migration), CC5+CL4 (security-critical updater
net.request, not live-verifiable here), CC7 (cosmetic arg-order), CC9 (zod dep),
CC10 (electron-store↔jsonStore split is deliberate for DPAPI), CC11 (config.ts
move), CC12 (file-tree reorg), CC13→L93, CC14→L142.
Verified: typecheck (node+web) + 275 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared UI primitives to end the "two ways to do X" drift on surfaces:
- EmptyState (ui/EmptyState.tsx): one centered empty block — optional focal
icon/badge + Body1 message + optional muted hint, with a `compact` variant
for in-content placeholders. Adopted by Downloads, History, Library (L116),
and — compact — the Terminal log, Diagnostics, and TemplateManager (L117).
- Banner (ui/Banner.tsx) + LinkSuggestion (ui/LinkSuggestion.tsx): one tinted
notification row (icon + truncating/wrap content + actions, tone brand/warning)
replacing five hand-rolled style blocks — the DownloadBar copied-link
suggestion / channel nudge / duplicate warning and the Library copied-link
suggestion (UI19).
- useTextStyles().truncate: shared single-line-ellipsis utility, adopted in
Library rows + the playlist panel + Banner; `title` now also sets minWidth:0
so it shrinks in a flex row (L126).
- L102: Settings "Default format" now uses the same SegmentedControl (Video/
Audio) + quality Select as the DownloadBar — one mental model; the combined
`kind|quality` string encoding is gone.
Resolved by verified convention (documented in code / CODE-AUDIT.md):
- L103 busy = in-button icon→Spinner everywhere (done in L134; no adjacent-
spinner pattern survives).
- L115 list-row actions icon-only+tooltip vs card-toolbar actions labelled.
- L120 Fluent <Card> for static content cards; tokenized surface recipe for
interactive/list-item cards.
- UI20 toggles = solid brand, list-selection = brand-tint (rule noted on
SegmentedControl); the cited Library pill is gone via UI18.
L128 (build-time DCE of preview seeds) deferred to Batch 14 with PERF8.
Verified: typecheck (node+web) + 268 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean config/copy items (Phase 7):
- L170: build.sourcemap 'hidden' on main/preload/renderer — emit .map for offline
crash symbolication (of the CC8 logger's raw stacks) without exposing them.
- L34: add `test:integration` npm script (AEROFETCH_REAL_DOWNLOAD=1) for the opt-in
real-download suite; stays out of `npm test` (needs network + live yt-dlp).
- L87: HistoryEntry carries the original `options`; redownload replays them so a
re-download reproduces the same post-processing instead of the current defaults.
- L96: folder "Browse" buttons -> "Browse…" (they open the OS picker).
- L97: PO-token placeholder "(none)" -> "Optional -- paste a token" (consistent).
- Ticked already-resolved: L99 (M28 aria-label), L113 (UX2 tooltip=aria-label),
L95 (usage follows verb/modifier rule), L172 (skipLibCheck accepted).
typecheck + 268 tests + eslint + prettier green (touched files LF/clean).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An App mount effect focuses #aerofetch-url when the Downloads tab is active
(double-rAF for paint), so a user can paste + type immediately instead of
clicking first.
typecheck + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- W2/UX19: windowState.ts persists the window's normal bounds + maximized flag to
<userData>/window-state.json (no new dep); createWindow restores from it with a
screen.getAllDisplays() visibility guard so it never reopens off a disconnected
monitor. Debounced save on resize/move + save on close (which may hide to tray).
- W10: notify() flashFrame(true)s the taskbar when a completion/failure lands while
the window is unfocused/minimized (Windows clears the flash on focus).
- W11: already implemented (badge.ts overlay dot + setOverlayIcon) — ticked.
typecheck + 268 tests + eslint + prettier green. (Window restore + flash want a
live quit/relaunch confirm.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the post-enqueue reset (URL + probe + one-shot trim/schedule/advanced/
incognito/options) into resetForm(), called by both download() and addPlaylist().
clearProbe() stays the lighter probe-only reset for onUrlChange. Also fixes a
latent inconsistency: addPlaylist previously left the trim/schedule/advanced
panels open after enqueuing a playlist.
typecheck + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- L138: gate the clipboard check on the settings `loaded` flag (not just
clipboardWatch), so it never reads using the pre-load fallback; the effect
re-runs when loaded flips true so a pre-launch copied link is still offered.
- L145: lastSeen is module-level, so a dismissed clipboard link stays suppressed
across the hook's remount on a Downloads<->Library tab switch.
- M14: SettingsView renders each card in a React-owned wrapper and drives its
visibility from a `hidden` state array — the search no longer writes display:none
onto a card's own DOM node. Matching still reads textContent (full label coverage,
no per-card keyword upkeep).
typecheck + 268 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- L94/PERF4: add queueSummaryOf — a 1-entry items-ref memo over summarizeQueue.
The store hands out a new items array per change, so App's taskbar subscription
and DownloadsView's render compute the aggregate once per change (second caller
hits the cache) instead of twice per tick. summarizeQueue stays pure/tested.
- PERF5: hoist VirtualList estimateSize/getKey/renderItem to stable module-level
functions so the virtualizer doesn't churn its measure/key cache.
- L163: one useShallow selection replaces QueueItem's 10 individual useDownloads
subscriptions (stable actions -> never re-renders the row).
typecheck + 268 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The queue/scheduler lived only in renderer memory, so saved/scheduled and other
pending downloads were silently lost on quit (audit M4). Persist them:
- main: queue.ts store (proven cached-atomic createJsonStore, queue.json) +
queue:list / queue:save IPC; QUEUE_MAX cap.
- shared: PersistedQueueItem = the durable subset of DownloadItem (runtime-only
progress/speed/eta/sizeLabel/finishing dropped).
- renderer: mirror the persistable items on every queue change (a signature over
the subset means progress ticks don't re-save; main's jsonStore coalesces the
writes) and rehydrate on launch via an App useEffect. Pure mappers extracted
to store/queuePersist.ts and unit-tested.
Restore semantics: downloading -> queued (yt-dlp continues the .part), saved +
scheduledFor survives so the promoter fires when due, paused/error return
actionable; completed/canceled are never persisted. A queueHydrated gate stops a
launch-time store change from wiping queue.json before it's read.
Reconciles ROADMAP.md. typecheck + 268 tests + eslint + prettier green.
(OS-level quit/relaunch cycle is worth a manual smoke test.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
schedule.ts keeps its own schtasks wrapper because it needs the numeric exit
code (getScheduledSync checks r.code === 0), which execFileAsync deliberately
doesn't surface. The 5 migrated sites are probe/ytdlp(x2)/ffmpeg/indexer/
download.probeMeta. Comment-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shouldAutoCheckYtdlp compared (now - lastCheck) >= interval; a backward system-
clock correction leaves lastCheck in the future so the difference is negative and
the daily check never comes due again. Treat a future lastCheck as skewed and run
the check. The renderer scheduled-item promoter is inherently fire-once (promotion
flips the item out of 'saved'), so it can't double-fire on a backward jump — a
forward jump firing early is documented and accepted as minor.
Unit-tested. typecheck + 263 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incognito lived only in the renderer (history skip), so a "private" download
still leaked in main: errorlog entries (title+URL), OS toasts showing the title,
and the signed-in cookies attached — the UI promised "no logging, no history, no
cookies" but only history held.
Plumb `incognito` through StartDownloadOptions and enforce it in main:
- logFailure skips the errorlog write (and the pre-spawn errorlog in ipc.ts).
- notify uses a generic outcome title and drops the detail body, so no title/URL
lands in the Windows Action Center.
- both cookie sources (--cookies login jar, --cookies-from-browser) are withheld,
so a private download can't be tied to the user's identity.
typecheck + 262 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-rolled `new Promise((resolve) => execFile(…, cb))` wrappers in
probe / ytdlp (×2) / ffmpeg / indexer.probeFlat / download.probeMeta with one
lib/exec.ts execFileAsync returning a normalized
{ok,stdout,stderr,timedOut,error} that never rejects. Each caller keeps its own
error mapping (timedOut -> "Timed out …" vs stderr) and JSON parsing; behaviour
is unchanged. windowsHide defaults on; per-call timeout/maxBuffer preserved.
Foundation for CC4/CC5 — the stdout line-buffer half lands with SIMP5, the
net.request half with SIMP16, at which point those checkboxes close.
Unit-tested (exec.test.ts) against the node binary: ok / stderr+nonzero-exit /
missing-binary / timeout. typecheck + 262 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnostics were invisible in a packaged build: main catches did a bare
console.error (DevTools suppressed in prod, M31) and the renderer's logError
(M29) wrote to an unreachable console. Add a small leveled logger
(src/main/logger.ts) appending to <userData>/logs/aerofetch.log with size-capped
rotation; all app/fs access is lazy+guarded so it's a safe no-op under tests.
- Route the 5 main-process console.* catch sites (jsonStore write-fail, settings
write-fail + plaintext-secret warn, cookie loadURL, yt-dlp auto-update)
through the logger.
- Add a fire-and-forget log:write IPC channel + preload logError() so the
renderer's logError forwards failures to the main file sink (closes the M29
"sink" half the code comments deferred to CC8). mockApi + Api type updated.
- Unit-test pure formatLine/composeMessage + no-op safety; update the R6
jsonStore test to assert against the logger.
The user-facing toast half of CC8 ties to the global status surface (UI25/UX9).
typecheck + 258 tests + lint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- L83: nsis.differentialPackage: false — the custom updater does full
downloads and never consumes the differential blockmap, so don't generate it
(NsisTarget gates blockmap generation on differentialPackage !== false).
- L84: add clean:dist script (fs.rmSync) run at the front of build:win so dist/
no longer accumulates old-version installers (~3 GB observed). dist/out were
already gitignored, so this is local-disk hygiene.
Config only; TS gate unaffected. 137 -> 135 open audit items.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tick items whose root cause already landed, and fix two doc-drift items:
- L82: align ROADMAP-PINCHFLAT Phase F Source/MediaItem sketch to the shipped
shared/ipc.ts shape (videoId/itemCount/watched/feedUrl + required
url/playlistTitle/playlistIndex).
- L89: note H8's afterAllArtifactBuild checksum hook fixes release-artifact
consistency going forward; stale dist artifacts handled by L84.
- Tick the wire-or-remove trio (M5/M6/UX1 — all wired in the DownloadBar
Advanced panel) and MAX_ENQUEUE_BATCH (M33 fixed the false comment).
- Tick UX ref-duplicates resolved by their roots (UX10=H5, UX13=M22, UX14=L76,
UX25=L78), the (ref) a11y line (UI33/UI30/UI28/UI29 all done), and the
(OK)/(non-goal) Windows-conventions line.
Docs only; no code touched. 149 -> 137 open audit items.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UI33 heading reset only needs to zero the UA margin — every heading in the
app carries a Fluent typography class that out-specifies this element selector
(and there are no raw headings), so the font-size/font-weight `inherit` lines
never applied and contradicted the comment. Keep just `margin: 0`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI31 — the native <Select> wrapper now takes a `disabled` prop that passes to
the <select> and applies disabled foreground/background/stroke tokens +
not-allowed cursor, so it can show a disabled state like Fluent controls.
UI32 — dropped the datetime-local's explicit colorScheme:'light dark' so it
inherits the resolved in-app scheme from the app root; the native calendar popup
now follows the app's Light/Dark theme, consistent with the native <Select>,
instead of tracking the OS preference.
W16 — the icon-only row-action clusters (QueueItem + HistoryView) enforce a
>=40x40px hit target via a `& button` min-size rule (glyph unchanged, subtle
button just gains padding). Labels were already non-hover-only: the shared Hint
shows on :focus-within and every action has an aria-label. LibraryView item rows
have no icon-only actions, so nothing there needed changing.
W18 — defended the two color-meaning surfaces against forced-colors: accent
swatches set forced-color-adjust:none (they're color previews; otherwise all
flatten to one system color), and the SegmentedControl active segment paints
with system Highlight/HighlightText under @media (forced-colors: active) so the
checked state stays visible. Status chips (Fluent Badge) and thumbnail tints
correctly let the system palette win.
typecheck + 253 tests + eslint + prettier + production build all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI33 — render titles as real headings so Narrator heading-navigation works:
the shared ScreenHeader title is now <Subtitle2 as="h1"> (one h1 per screen in
one place — Downloads/History/Library/Settings/Terminal); DownloadsView "Queue
(N)" and all 11 settings cards are <Subtitle2 as="h2"> sections; Onboarding
"Welcome" is <Title2 as="h1">. A minimal h1–h6 margin reset in base.css zeroes
the UA heading margin (Fluent's typography classes out-specify the element
selector, so the visual ramp is unchanged) so layout matches the former spans.
UI27 — give the command palette proper combobox/listbox semantics: the input is
role="combobox" with aria-controls/aria-activedescendant/aria-autocomplete, the
list is role="listbox", and each row is role="option" with aria-selected, so the
active row is announced to screen readers via active-descendant. Focus stays on
the input (standard combobox pattern), so the rows became non-focusable divs;
onMouseEnter is kept to unify pointer + keyboard highlight on one sel state.
Also corrected UI30's checkbox in the audit (already implemented via the shared
SegmentedControl radiogroup; only the marker lagged).
typecheck + 253 tests + eslint + prettier + production build all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The extracted hook relied on inferred return typing, inconsistent with the
codebase convention (useClipboardLink: ClipboardLink, useResolvedDark: boolean).
Add a compiler-verified DownloadBarController interface and annotate the hook —
the returned object is checked against it, so the surface can't silently drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the 858-line DownloadBar (~17 interdependent state hooks):
- downloadBar/useDownloadBar.ts: all state and behaviour (probe, playlist,
trim/schedule, per-download options, dup guard, drag-drop, command preview),
exposing wrapper handlers so preview-invalidation side-effects stay put.
- downloadBar/styles.ts: the makeStyles block.
- downloadBar/PlaylistPanel.tsx: the playlist-selection panel.
DownloadBar is now a render-only shell driven by the hook. `usingFormats` no
longer narrows `info` across the hook boundary, so the format Select guards with
`info?.formats ?? []` (only reached when usingFormats is true). Behaviour
unchanged; full typecheck + lint + build + 248 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the 1104-line SettingsView (~11 cards, ~30 selector subs, ~20 useState)
into components/settings/*.tsx, each owning its own store subscriptions, local
state, and handlers:
- DownloadsCard, AppearanceCard, PostProcessingCard, NetworkCard, CookiesCard,
CustomCommandsCard, FilenamesCard, BackupCard, DiagnosticsCard,
SoftwareUpdateCard, AboutCard
- settingsStyles.ts holds the shared makeStyles hook.
SettingsView is now a ~90-line shell: the search box + rootRef filter and the
card list. Each card still renders exactly one <Card> DOM node, so the
display-toggle search filter is unchanged. Behaviour identical; 248 tests +
typecheck + lint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract from the 614-line store:
- downloadTypes.ts: all public types (DownloadItem, AddOptions, AddEntry,
ChosenFormat, DownloadStatus, DownloadState). Re-exported from downloads.ts
so every `import { ... } from '../store/downloads'` site is unchanged.
- downloadItem.ts: buildItem/titleFromUrl + the RESOLVING placeholder.
- downloadSeed.ts: the preview-only mock seed array.
downloads.ts now holds only the store, recordCompletion, and the preview
ticker. Behaviour unchanged; 248 tests + typecheck green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>