Commit Graph

169 Commits

Author SHA1 Message Date
debont80 27cae28e12 feat(diagnostics): persist full stderr detail behind a "Show details" toggle
describeDownloadError condenses a postprocessing failure to one line for
the queue/notification, discarding the rest of yt-dlp's stderr. The
persisted error log now also keeps the fuller non-noise transcript
(stderrDetail) as an optional `detail` field, and the Diagnostics card
gets a collapsible "Show details" expander per row so a failure like
"Postprocessing: Conversion failed!" can be inspected -- and copied
into a bug report -- without re-running the download from a terminal.
2026-07-05 14:56:09 -04:00
debont80 6cb9c5ca47 fix(downloads): re-resolve options on retry instead of replaying stale ones
A queued item freezes its post-processing options at enqueue time, so
retrying a failed download replayed whatever was in effect when it was
first added -- a settings change made afterward (e.g. turning
SponsorBlock-remove off, which forces a slow, failure-prone software
re-encode) never took effect on retry.

retry/retryAll now re-resolve a source-tied item's options from the
current source profile / global settings (the same precedence enqueue
uses) before requeuing it. Manual pastes with no source keep their
explicit per-download options untouched, and this is deliberately
distinct from the history re-download path, which keeps frozen options
on purpose to reproduce an identical file.
2026-07-05 14:39:45 -04:00
debont80 5de4b40c1d fix(download): name the failing step in postprocessing errors
yt-dlp collapses an ffmpeg postprocessing failure to a bare
"Postprocessing: Conversion failed!" with no indication of which step
died, and without --verbose the underlying ffmpeg reason is never
emitted at all. describeDownloadError() appends the last non-noise
trailing lines (e.g. the [ModifyChapters] SponsorBlock re-encode) so
the error log and completion notification point at the actual cause
instead of a dead end.
2026-07-05 14:39:29 -04:00
debont80 1be2708d15 chore: bump version to 0.6.1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.6.1
2026-07-03 08:20:39 -04:00
debont80 037ea2da32 feat(updater): bake a read-only update token into the build
The release repo is private, so the auto-updater previously required each
user to paste a Gitea token into Settings before it could see or download
updates. Inject a read-only token at build time (AEROFETCH_UPDATE_TOKEN env
var or a gitignored .update-token file) via electron.vite's `define`, and
fall back to it in authHeader() when the user hasn't set their own. The token
value lives only in the built bundle, never in source or git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:20:21 -04:00
debont80 48ce591702 chore: bump version to 0.6.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.6.0
2026-07-03 05:55:40 -04:00
debont80 e29374dcfa docs: scrub dangling references to deleted roadmap docs
The ROADMAP/ROADMAP-PINCHFLAT docs were removed; this drops the now-dead
"see ROADMAP-PINCHFLAT.md" breadcrumbs (and one "the roadmap" mention)
from source comments, preserving the surrounding Pinchflat-style context.
Comment-only; typecheck (node+web) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:16:32 -04:00
debont80 ff9077989f docs: remove completed roadmap + release-gate docs
The ROADMAP files are fully shipped/retrospective and the smoke-test and
signing guides are retired at the maintainer's request. resources/bin/README.md
is kept (documents bundled-binary setup + license obligations).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:13:57 -04:00
debont80 6cf6c0ead6 Merge origin/main (PR #10) into local main 2026-07-02 23:04:21 -04:00
debont80 e7fa602d73 Merge docs/code-audit-stale-deferred — retire completed CODE-AUDIT.md 2026-07-02 23:02:24 -04:00
debont80 2988404dcf docs(audit): remove completed CODE-AUDIT.md
The audit is fully closed (0 open / 372 done); all findings shipped to main across Batches 6-28. Removing the 2,558-line tracking doc now that it holds no open items. The preview-probe harness (.claude/launch.json) is retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:02:01 -04:00
debont80 6812939848 Merge pull request 'docs(audit): fix stale Deferred entries for metadata editing + PO-token auto-mint' (#10) from docs/code-audit-stale-deferred into main
Reviewed-on: #10
2026-07-02 17:10:10 -04:00
debont80 3623418fe3 docs(audit): fix stale Deferred entries for metadata editing + PO-token auto-mint
Both shipped (ROADMAP.md Phase L/P) but CODE-AUDIT.md's Deferred section
still listed them as pending.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 17:08:20 -04:00
debont80 d8df5ea7c8 Merge pull request 'feat(playlist): exact-format-per-item selection' (#9) from feat/playlist-per-item-format into main
Reviewed-on: #9
2026-07-02 17:06:44 -04:00
debont80 15c2229539 fix(playlist): drop stale per-item formats on re-probe
Peer-review of the exact-format-per-item feature: re-clicking "Preview
formats" on an already-loaded playlist reset info/playlist but left
itemFormats/itemFormatOptions/formatExpanded keyed by position, so a
stale exact-format pick could silently reattach to a different video
landing at the same index in the new probe result and flow into
addPlaylist(). Extracted resetPlaylistItemState() and call it from
both clearProbe() and fetchFormats().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 17:05:29 -04:00
debont80 4c2b54a599 feat(playlist): exact-format-per-item selection
Each video row in the playlist panel gets an exact-format button that
lazily probes just that entry's URL (reusing the existing single-video
probe IPC, no new main-process code) and shows a native Select of its
real formats. The choice flows through addMany -> formatId per queue
item. Probing is on-demand per row, never all up front, since a
playlist can hold thousands of entries and per-entry probes are
rate-limited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 16:55:58 -04:00
debont80 5c88d9c8e0 feat(network): request-throttle controls to avoid YouTube rate-limiting
Re-implements the sleepRequests/sleepInterval/maxSleepInterval feature
(--sleep-requests / --sleep-interval / --max-sleep-interval) from the
stale feat/binary-management-and-library-scale branch, ported onto
current main's structure (zod settingsSchema, decomposed NetworkCard).
All default to 0/off, preserving current behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:34:34 -04:00
debont80 61d5261e33 docs: flip stale PINCHFLAT output-path checkbox (implemented Batch 26)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:16:45 -04:00
debont80 a8dc89d6fc Merge Batch 28 — Retention & quality-upgrade 2026-07-02 16:14:24 -04:00
debont80 0956bd536a feat(pinchflat): Retention & quality-upgrade per Library source
On-demand maintenance panel (SourceRetention) on the source detail —
not a persisted schedule (roadmap's niche-for-desktop scope).

Prune: keep the newest N downloaded files, delete the rest's files
(download archive prevents silent re-download); dry-run count + two-step
confirm before any deletion. Upgrade: re-enqueue items whose recorded
downloadedQuality ranks below the source's current target (profile or
global). Selection is pure + injected-now in @shared/retention.ts
(selectPruneCandidates/selectUpgradeCandidates + qualityRank), unit-
tested; pruneMediaItems (main) deletes best-effort and clears state.
MediaItem.downloadedQuality now recorded through the completion path.
Live-verified the panel renders. 366 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:14:23 -04:00
debont80 15cc96dd10 Merge Batch 27 — Media Profiles 2026-07-02 16:02:09 -04:00
debont80 ed1daf8bef feat(pinchflat): Media Profiles — named download presets per Library source
A MediaProfile bundles { kind, quality, options, outputTemplate, extraArgs }
that a Source points at via Source.profileId, overriding the global defaults
field-by-field for that source's downloads.

Backend mirrors CommandTemplate: profiles.ts (createJsonStore, PROFILES_MAX,
isProfileLike + sanitize), CRUD IPC (profiles:list/save/remove) + preload +
useProfiles store; Source.profileId + sources:set-profile. Pure resolution
in @shared/profileResolve.ts (profile → global, per field; unit-tested).
Applied in the sources-store enqueueItems; a per-download collectionOutput-
Template override threads to buildArgs so a profile's outputTemplate wins.

UI: ProfileManager (mirrors TemplateManager) in a new Settings → Advanced →
Media profiles card, plus a per-source Profile picker in the Library source
detail. Both live-verified in the preview; 354 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:02:07 -04:00
debont80 a02953f6d1 Merge Batch 26 — roadmap surfacing + output-path setting 2026-07-02 15:44:44 -04:00
debont80 42b7b2e01f feat(roadmap): surface stale checkboxes + collection output-path setting
Command preview + incognito toggle were already wired end-to-end in the
DownloadBar (Show command button, Incognito checkbox) — flipped the two
stale ROADMAP.md checkboxes with notes; verified live in the preview.

Output-path setting (PINCHFLAT): new Settings.collectionOutputTemplate — a
raw yt-dlp -o template overriding the default Channel/Playlist/NNN-Title
layout for Library downloads (empty = default). collectionOutputTemplate()
takes an optional custom template (joined under outDir, left for yt-dlp to
expand); download.ts passes the setting; zod schema validates it as a safe
relative template; Settings -> Filenames field. Tests in buildArgs +
settingsSchema; live-verified the field renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:44:44 -04:00
debont80 72feaa62f8 Merge Batch 25 — project organization (CC12, CC7, CC10) 2026-07-02 15:37:28 -04:00
debont80 9134e7d216 feat(audit): Batch 25 — project reorg (CC12), leading DI args (CC7), CC10 by-design
CC12: renderer views/ (5 screens + Onboarding + settings cards) +
lib/ (theme/thumb/datetime/id/qualityOptions/useClipboardLink/queueStats);
main core/ (buildArgs/validation/indexerCore/ytdlpPolicy). git mv preserved
history; all src+test imports updated. Build emits view chunks from views/,
344 tests + typecheck green, live probe rendered all screens.
CC7: wc/onProgress is the leading arg everywhere — downloadAppUpdate(wc,url),
indexSource(onProgress,url,signal), indexSourceCancelable(onProgress,url).
CC10: closed by-design — jsonStore backs all records; settings stay in
electron-store for DPAPI secret encryption (a deliberate two-store split).
Also closes the UI24/W4 context-menu cross-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:37:28 -04:00
debont80 f45acb645b Merge Batch 24 — zod validation layer (CC9) 2026-07-02 12:41:01 -04:00
debont80 8f6b2a761b feat(audit): Batch 24 — one validate-and-coerce schema layer for settings (CC9)
zod field schemas in settingsSchema.ts replace setSettings' bespoke
per-key switch: parse → skip-on-fail → store, side effects (secret
encryption, launch-at-startup sync) kept in settings.ts. sanitizeOptions
extracted to settingsOptions.ts to break the import cycle; backup import
flows through the same schema. Field-by-field parity pinned in
test/settingsSchema.test.ts (15 tests); full suite unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:41:00 -04:00
debont80 6abed2064d Merge Batch 23 — naming & config (CC1, CC11) 2026-07-02 12:34:05 -04:00
debont80 134c6a167c feat(audit): Batch 23 — settings key renames with migration (CC1) + config.ts (CC11)
CC1: customCommandEnabled→enableCustomCommands, downloadArchive→
useDownloadArchive, clipboardWatch→watchClipboard, sidebarCollapsed→
isSidebarCollapsed, hardwareAcceleration→useHardwareAcceleration,
videoDir/audioDir→videoFolder/audioFolder (+ chooseDir/clearDir store
actions → chooseFolder/clearFolder). Rename map in settingsMigration.ts
(pure, unit-tested incl. pre-rename backup fixture), applied as an
idempotent on-disk shim at store open and on backup import so old
settings.json and old backups both keep working.

CC11: update host/owner/repo + release API moved to src/main/config.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:34:05 -04:00
debont80 545cfeed5e Merge Batch 22 — perf close-out 2026-07-02 12:25:22 -04:00
debont80 384870ee4a feat(audit): Batch 22 — perf close-out (PERF3 benchmarked, PERF6/L162 by-design)
PERF3: pumpBench.test.ts pins pump()'s pipeline cost — 0.43ms @5k items,
1.47ms @20k (median) — negligible at a few events/sec, so the
count+pointer rewrite of the core queue is declined as risk without
reward; the benchmark stays as a 5ms regression tripwire.
PERF6/L162: closed as documented-by-design (virtualization bounds
mounted thumbs; a theme prop conflicts with PERF5's stable renderer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:25:21 -04:00
debont80 ea52735b12 docs(audit): check off CL4 + CC5 (Batch 21)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:21:57 -04:00
debont80 e9d41b00ea Merge Batch 21 — updater refactor (CL4, CC5) 2026-07-02 12:21:29 -04:00
debont80 34fbf36610 feat(audit): Batch 21 — updater refactor under behavior pins (CL4, closes CC5)
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>
2026-07-02 12:21:28 -04:00
debont80 d1e3168449 Merge Batch 20 — Windows/main-process (UI23, W12, W15, L131) 2026-07-02 12:15:52 -04:00
debont80 3b0d668f6c feat(audit): Batch 20 — Windows/main-process (UI23, W12, W15, L131)
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>
2026-07-02 12:15:51 -04:00
debont80 f54d9a1965 docs(audit): check off UX7/UX8/UX23 (Batch 19)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:01:15 -04:00
debont80 fc2db1e1b6 Merge Batch 19 — Settings IA (UX7, UX8, UX23) 2026-07-02 12:00:47 -04:00
debont80 a5885739d2 feat(audit): Batch 19 — Settings information architecture (UX7, UX8, UX23)
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>
2026-07-02 12:00:47 -04:00
debont80 93f0091104 Merge Batch 18 — watched visual pass 2026-07-02 11:55:45 -04:00
debont80 cdb6eea437 feat(audit): Batch 18 — watched visual pass (UI3, UI12, UI16, UI21, UX18, L109, UX26)
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>
2026-07-02 11:55:45 -04:00
debont80 747cecd1e0 Merge Batch 17 — layout & virtualization (L161, L104) 2026-07-02 11:43:36 -04:00
debont80 17d42ed4fe feat(audit): Batch 17 — screen scroll contract + History virtualization (L161, L104)
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>
2026-07-02 11:43:34 -04:00
debont80 dbbb0c70f9 chore(format): endOfLine auto so format:check is stable under git autocrlf
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>
2026-07-02 11:13:07 -04:00
debont80 71b88e2304 Merge Batch 16 — store discipline (L142, L93, CC13, CC14) 2026-07-02 11:10:58 -04:00
debont80 2ef19ead71 feat(audit): Batch 16 — store discipline (L142 reconciliation, L93 view-model hooks)
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>
2026-07-02 11:10:49 -04:00
debont80 96800d5a17 feat(audit): L51 + L64 — daily-sync time picker + aria2c connection tuning
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>
2026-07-01 22:57:20 -04:00
debont80 e19f988c72 feat(audit): Batch 15 live-verify — download-engine lifecycle (R4, L65, L139, B2, B6, L143)
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>
2026-07-01 21:36:22 -04:00
debont80 b3e5e4f309 Merge audit chain (Batches 6-15) into main
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>
2026-07-01 18:35:40 -04:00