- 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>
The prior commit kept a pass-through `export { fmtBytes, fmtSpeed, fmtEta }` in
main/lib/formatters.ts purely so existing importers wouldn't have to change —
which re-exported fmtSpeed (zero importers, dead) and left download.ts re-
exporting fmtBytes/fmtEta with no consumer at all, undercutting the "one home"
goal.
Now the real consumers import from the canonical module directly:
- probe.ts and the download.test import from @shared/format.
- formatters.ts keeps only its internal `import { fmtBytes }` (for parseProgress)
and no longer re-exports.
- download.ts re-exports only parseProgress (its own function); the dead
fmtBytes/fmtEta re-export is removed.
No behavior change. typecheck + 248 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes H4 and the remaining formatter half of H2.
- New @shared/format.ts is the single home for fmtBytes/fmtSpeed/fmtEta, imported
by both main and renderer. main/lib/formatters.ts re-exports them.
- DownloadProgress now carries raw speedBytesPerSec/etaSeconds instead of
pre-formatted speed/eta strings. main's parseProgress emits the raw numbers.
- The renderer stores the raw numbers on DownloadItem; QueueItem formats them for
per-item display, and summarizeQueue sums/maxes them directly. The lossy
string->number->string round-trip in queueStats (parseSpeed / parseEtaSeconds /
a local formatSpeed / formatEta) is deleted, along with a duplicate fmtEta in
store/downloads.ts.
- Per-item and aggregate speed now use the same 1024-based scale; the aggregate
previously used a 1000-based formatter (a latent inconsistency).
Tests updated for the raw-number contract (parseProgress, summarizeQueue).
typecheck + 248 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates the scattered YouTube-URL parsing (the clean, renderer-contained
half of H2):
- Move the complete youtubeId (watch/shorts/embed/live/youtu.be) into
lib/urlHelpers.ts as the single home.
- thumb.ts, queueStats.ts (sameVideo), and store/downloads.ts (titleFromUrl)
now import it; the three ad-hoc reimplementations are removed.
- titleFromUrl gains correctness: it previously labeled ANY host with a ?v=
param as "YouTube video (…)" and missed youtu.be/shorts; it's now host-correct
and covers every YouTube shape. sameVideo now also dedupes /shorts/ID against
/watch?v=ID.
- Unit-tested in test/clipboardLink.test.ts (+4 cases, 247 pass).
The formatter half of H2 is deferred to travel with H4: the renderer re-parses
speed/eta strings only because raw numbers aren't carried across the IPC boundary
(H4), and fmtBytes (1024) vs formatSpeed (1000) differ intentionally.
typecheck + 247 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Housekeeping: the command-preview / incognito / per-download-options features
were implemented and self-reviewed in the previous commits but their audit
checkboxes were still open. Marks M5, M6, UX1 done with fix notes. Completes the
SHOULD-fix-for-1.0 release-gate tier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes found reviewing the previous commit:
- M5 preview was inaccurate: loadCommandPreview omitted formatId/formatHasAudio
and trim, so the "exact command" differed from what download() actually spawns
whenever a probe-selected format or a trim range was set. toggleCommandPreview
now mirrors download()'s StartDownloadOptions (format + trim included).
- Stale preview: a shown command only cleared on URL change. Now it also clears
on kind, quality, format, trim, download-options change, and on probe — so the
displayed command is never out of sync with the form.
- Command button is now a real toggle (Show command / Hide command) instead of a
no-op "Command · shown" label.
- Per-download overrides are now one-shot: incognito + downloadOptions reset to
defaults after a download, matching trim/schedule and preventing incognito from
silently carrying into the next download.
- Style: replaced two inline style={{}} usages with makeStyles classes
(commandError), hardcoded 'monospace' -> tokens.fontFamilyMonospace, dropped an
unnecessary fragment, and let long command lines wrap (pre-wrap/break-all).
typecheck + 243 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M5 (Command preview):
- Added 'Show command' button in DownloadBar that calls window.api.previewCommand()
- Displays the exact yt-dlp command line that will run for the current form state
- Helps users verify the exact behavior before download
M6 (Incognito mode):
- Added incognito checkbox in Advanced panel
- Passes through AddOptions to enable no-logging, no-history, no-cookies mode
- QueueItem already shows 'Private' badge when item.incognito is set
UX1 (Per-download options):
- Expanded Advanced panel now shows full DownloadOptionsForm
- Allows overriding audio format, video codec, container, embed-subtitles,
SponsorBlock, format-sort for any single download (not just global settings)
- Options passed through AddOptions.options to startDownload
- Same form component used for both Settings defaults + per-download overrides
UI additions:
- Advanced toggle button (Settings icon) in DownloadBar
- Collapsible Advanced panel with incognito checkbox + DownloadOptionsForm
- Command preview panel shows the yt-dlp argv (monospace) or error message
- Both features only show for single videos (not playlists)
All plumbing was already in place (previewCommand IPC, StartDownloadOptions.options,
AddOptions.incognito, mockApi); only the renderer UI was missing.
typecheck + 243 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Critical:
- C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS
in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all
derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx
shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts.
- C2: break the downloads<->sources circular import via a typed event bus
(store/coordinator.ts). App eagerly imports sources for side-effects so startup
load + scheduled --sync + downloadCompleted subscription still run.
Reliability:
- L140/L148: cancel/pause release the active slot synchronously; identity-guarded
releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected
by a stale entry nor run over the concurrency cap.
- L141: renderer history capped at 500 + url de-dup to match main.
- R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry
instead of an empty catch.
- R7: encryptSecret warns before the plaintext fallback.
typecheck + 243 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review of this session's audit batch (L106/L107/L122, L119/L151/L153, M3, M17,
M18, M23/M27, R8, L62). One issue found and fixed:
- SettingsView copyErrorReport comment quoted the old "No errors logged."
empty-state string, which L119 renamed to "No errors yet." Reworded the
comment to not quote a specific string.
Everything else verified sound: M3 recordCompletion is a faithful extraction
(+ a correct canceled-item bugfix); M17 status precedence; M18 migration; M23
per-source budget cap; M27 enqueueKind state is safe (only the expanded source
renders its detail); R8 pretty flag; L62 drop of the synthetic ext has no
consumers. No BOM churn. The incidental em-dash->-- conversions in
jsonStore/sources comments leave each file internally consistent and aligned
with the project's ASCII convention, so they stand.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-best format option claimed ext:'mp4', but the real output container
for the best pick depends on the user's videoContainer setting (mp4/mkv/webm),
so it mislabeled mkv/webm outputs. ext is optional on FormatOption and the
option's label is the fixed "Best available", so the field is simply omitted.
hasAudio:true is kept (the best pick always merges an audio track).
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
writeJsonAtomic always pretty-printed (JSON.stringify(value, null, 2)), which
needlessly inflates size and write time for the media-items store (up to
MAX_ITEMS=20000 rows). Added an optional `pretty` flag (default true, so
history/sources/templates/errorlog stay hand-inspectable) threaded through
createJsonStore, and set it false for media-items.json.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M23: replaceMediaItems wrote [...newItems, ...others] and the JSON store sliced
the combined list to MAX_ITEMS, so re-indexing one large source could
silently evict ANOTHER source's tail. Now other sources are always kept in
full and only the source being (re)indexed is capped to the remaining
budget (MAX_ITEMS - others.length).
M27: enqueueItems forced settings.defaultKind for every item, so a channel
couldn't be queued as audio without flipping the global default. Added an
optional `kind` override (falls back to the default) and a video/audio
SegmentedControl in the Library action row, seeded from defaultKind so
existing behavior is unchanged until toggled.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audio quality preset "Best (MP3)" named a format that's wrong whenever the
audio format is opus/flac/wav. Renamed it to "Best" so the label never
contradicts the chosen audioFormat (which is picked separately). Bitrate presets
stay as bitrates and are already ignored for lossless formats (M21).
- AUDIO_QUALITY_OPTIONS[0]: 'Best (MP3)' -> 'Best' (buildArgs already had a
`case 'Best'` returning '0', so arg generation is unchanged).
- Updated the main + renderer defaults and the preview/mock/test fixtures.
- Added a one-line migration in getSettings() that rewrites a stored legacy
'Best (MP3)' to 'Best' so existing users' dropdowns match.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LibraryView mapped url -> status by last-write, so when a URL had multiple
queue entries ("Download anyway"), the row reflected whichever item was
iterated last. Added a STATUS_PRIORITY ranking (completed > downloading >
queued > saved > paused > error > canceled) and keep the highest-priority
status per URL, so a row shows the most meaningful state of that video.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The completion side-effect (add to history unless private + markDownloaded on
the source MediaItem) was duplicated in applyEvent('done') and the preview
fake-ticker, and had already drifted (the preview copy omitted formatId/
formatHasAudio). Extracted one recordCompletion(item) helper that both call.
Also tightened a latent bug: the old applyEvent ran markDownloaded for any
'done' event with a mediaItemId, even when the item was canceled (the reducer
keeps canceled items as 'canceled'). recordCompletion now runs only when
status === 'completed', so a canceled collection item is no longer wrongly
marked downloaded (and will be retried on the next re-sync).
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L119: Standardized empty-state copy on the "No X yet." pattern. Terminal
"Output will appear here." -> "No output yet."; Diagnostics
"No errors logged." -> "No errors yet."
L151: Range-dash convention is ASCII hyphen everywhere. Changed the lone
en-dash prose range ("2-3 is a good balance") to a hyphen, matching the
time-range placeholders (1:30-2:00).
L153: The three "Dismiss" close buttons now name what they dismiss --
"Dismiss suggested link" (DownloadBar + Library suggestion banners) and
"Dismiss duplicate warning" (DownloadBar dup row).
L95 left open: the Cookies-card "sign in"/"sign-in" usage is already correct
English (verb vs modifier); no change warranted.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L106: Established the convention — ArrowClockwise = retry/redo a local action
(retry-all, re-download, retry queue item); ArrowSync = network sync/
update (sync sources, reindex, check/apply updates). Flipped the two
misclassified sites (LibraryView "check for new" sync, SettingsView
"Update yt-dlp") to ArrowSync and dropped the now-unused imports.
L107: QueueItem queued-row Spinner was size="extra-tiny"; every other spinner
is "tiny". Standardized to "tiny".
L122: The app-update and yt-dlp-update buttons showed "Checking…"/"Updating…"
in the label AND a separate sibling Spinner. Folded the spinner into the
button icon (icon swaps to Spinner while busy), matching DownloadBar/
LibraryView, and removed the redundant sibling Spinner.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addressing issues found reviewing this session's audit commits:
REGRESSIONS FIXED
- updater.ts (L52): reverted the spawn(detached) installer launch. Raw
CreateProcess/spawn fails with ERROR_ELEVATION_REQUIRED if the NSIS build
flips to perMachine (the config comment invites this), and it silently
dropped shell.openPath's launch-error detection. Restored shell.openPath
(ShellExecute honors the elevation manifest) and kept the L52 goal by
replacing the 1500ms timer with setImmediate(app.quit).
- LibraryView.tsx (M15): native <button> doesn't inherit color (UA sets
ButtonText), so the source-card chevron (currentColor) would render wrong
in dark mode. Added color: colorNeutralForeground1 to cardHead.
INEFFECTIVE CHANGE REVERTED
- base.css (W18): forced-color-adjust:auto on * is the CSS default (no-op),
and [class*="backdrop"] never matches Fluent/Griffel's hashed atomic class
names. Reverted; W18 unmarked (needs real High-Contrast testing).
WEAK FIX REVERTED
- Onboarding.tsx (L67): "Skip" and "Get started" called the identical handler
on a single-screen onboarding. Removed the duplicate button; L67 unmarked
(real ask is a "show tips again" revisit affordance).
STYLE / CORRECTNESS
- Stripped UTF-8 BOMs accidentally added to Select/CommandPalette/App/
Onboarding by the PowerShell sanitizer; left pre-existing BOMs untouched.
- DownloadBar.tsx: merged the duplicate @shared/ipc import; removed a stray
double blank line.
- qualityOptions.ts (L29): restored the original `satisfies Record<MediaKind,
readonly string[]>` constraint + the noUncheckedIndexedAccess rationale
comment (the extraction had dropped both for a weaker `as const`).
- downloads.ts: removed double blank line left by the QUALITY_OPTIONS move.
- binaries.ts: corrected YTDLP_MISSING_MSG doc comment ("at startup" -> the
actual call sites).
typecheck + 242 tests + eslint all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
firstUrl() in DownloadBar was a local duplicate of looksLikeUrl iteration
(scanning multi-line text for the first valid URL). Promoted to
firstUrlInText() in src/renderer/src/lib/urlHelpers.ts alongside the other
URL utilities, re-exported from useClipboardLink.ts. DownloadBar now imports
firstUrlInText from there instead of maintaining a local copy.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L67: Onboarding now has a 'Skip' button alongside 'Get started', both in a
right-aligned flex row. Both call update({hasCompletedOnboarding:true})
so the behavior is the same — Skip just reads as optional for users who
want to get started immediately without reading the tips.
L86: HistoryView now clears any selected IDs that are no longer visible when
the filter (query or kind) changes. A useEffect on `filtered` intersects
the current selection with the new visible set and calls setSelected only
if the size actually changed (avoids re-renders when the filter doesn't
affect the selection).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.claude/launch.json was committed but VS Code reads launch configs from
.vscode/launch.json, so no tool was consuming it. Moved to the correct
location and removed the misplaced file from git tracking.
Also added .vscode/settings.json to .gitignore so local VS Code user
preferences don't end up tracked.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W18: Added explicit forced-color-adjust:auto on the universal selector so
the browser's HC mode can override all custom token colors. Added an
@media(forced-colors:active) rule that replaces the CommandPalette
backdrop's rgba(0,0,0,0.32) with the Canvas system color + a ButtonText
border, so the palette remains perceivable under Windows High Contrast.
W19: Already fixed in the SR8 commit (window title now reflects download
state via mainWindow.setTitle in the taskbarProgress handler). Marking
done here with the companion CSS change.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cleanError() strips leading 'ERROR:' from yt-dlp stderr before sending
errors to the UI and error log. The browser-preview seed entry in
store/errorlog.ts had the raw 'ERROR: [youtube]...' prefix, making the
preview inconsistent with what production shows. Removed the prefix from
the seed string so the preview and production rendering match.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The window title was always 'AeroFetch' regardless of activity, so screen
readers and taskbar previews couldn't tell whether downloads were running.
The taskbarProgress IPC handler (which already fires on each progress tick)
now calls mainWindow.setTitle() alongside setProgressBar/setOverlayIcon:
- Idle: 'AeroFetch'
- Downloading: 'AeroFetch -- N downloads active'
- Error: 'AeroFetch -- Error'
This reuses the same overlay badge label string so the text is consistent
between the taskbar tooltip and the title bar.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W8: Added Ctrl+, as a standard Settings accelerator alongside the existing
Ctrl+K palette shortcut. The key handler now handles both in one listener
so there's a single window.addEventListener lifecycle.
W9: Command palette now restores focus to the previously focused element when
it closes. prePaletteRef stores document.activeElement at open time;
onClose fires a requestAnimationFrame focus-restore after unmounting the
palette, so the focus returns to the trigger (a sidebar nav button, the
URL input, etc.) rather than leaving focus stranded at the document body.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both interactive headers in LibraryView used role="button" on divs, which is
invalid when they contain child interactive elements (group header had nested
All/None and Download buttons).
- Group header (groupHead): split into an outer flex div + a native <button>
(groupToggle style) for the toggle/expand action + sibling Fluent Buttons
for All/None and Download. The toggle button gets aria-expanded and a
composite aria-label (title + item count). e.stopPropagation() removed
from the sibling buttons since they are no longer inside the toggle.
- Source card header (cardHead): converted from a role="button" div to a
native <button> with width:100%, border:none, and background:transparent.
aria-label set to source.title. tabIndex/onKeyDown removed (native button
handles keyboard by default).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both DownloadBar (renderer, drag-drop) and deeplink.ts (main, argv) had their
own regex to extract URL= from a Windows Internet Shortcut file -- with subtly
different patterns (/^\s*URL\s*=\s*(\S+)/ vs /^URL=(.+)$/).
Added parseUrlShortcutContent() to @shared/ipc. Both callers now delegate to
it and apply their own http/https guard (looksLikeUrl in renderer, asHttpUrl
in main). The local parseUrlFile wrapper in DownloadBar is kept as the
validation compositing point; it's now a one-liner.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts,
coupling views to the store. Moved to src/renderer/src/qualityOptions.ts,
which assembles it directly from @shared/ipc. store/downloads.ts no longer
imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView)
updated to import from the new module.
L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a
detached spawn. The installer is spawned with stdio:'ignore' and unref'd so
it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate
so the IPC response returns to the renderer before the process exits.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L19: App.tsx replaced `setTimeout(() => focus(), 60)` in the "New download"
palette action with `requestAnimationFrame(() => requestAnimationFrame(...))`
to wait exactly one React render + paint cycle rather than an arbitrary 60ms
timer that could race on slow machines.
L73: DownloadBar's probe button label changed from "Fetch" to "Check URL" (hint
and aria-label both updated). Placeholder text updated to match. This
distinguishes it from LibraryView's "Index" action, which catalogs an entire
channel/playlist, making both labels clearly meaningful in context.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L46: Introduced YTDLP_MISSING_MSG constant in binaries.ts. All four callers
(download, indexer, probe, ytdlp) now emit the same user-facing string:
"yt-dlp.exe is missing. Open Settings -> Software update to re-download it."
This removes newline-embedded messages and four divergent phrasings.
L71: Removed readOnly from videoDir/audioDir folder inputs in SettingsView.
Added onChange handler so users can paste or type a path directly without
being forced to use the Browse button. The path still flows through
applySettings() in main, which validates it.
L74: Accent color swatches were triple-labeled (aria-pressed + aria-label +
title). Replaced with a single aria-label that embeds "(selected)" state,
plus title for the hover tooltip. Removed the incorrect aria-pressed
(these are selection buttons, not toggles).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>