Skip to content

fix: close the three plan gaps whose visible half was missing (3.7, 6.5, 8.12) - #17

Merged
protonspy merged 3 commits into
mainfrom
fix/plan-gaps
Aug 1, 2026
Merged

fix: close the three plan gaps whose visible half was missing (3.7, 6.5, 8.12)#17
protonspy merged 3 commits into
mainfrom
fix/plan-gaps

Conversation

@protonspy

@protonspy protonspy commented Aug 1, 2026

Copy link
Copy Markdown
Owner

An audit of plans/open-wiki.md against the code found three tasks ticked [x]
whose backend, IPC and tests all existed but whose user-facing half did not.
This closes them, plus the small untruths found alongside.

What changed

6.5 — a page says which sources it came from. sourcesOfPage had a handler,
a channel and tests; no component called any of them, so the feature was done
from every angle except the user's. It now renders under the frontmatter,
carrying the source's title and somewhere to click rather than the bare id.
Clicking opens the panel a provenance link opens (8.6) at the source's own
start — p1 for a document, 0:00 for a recording, which are the anchors
pdf.ts and 4.13 actually write. A citation whose source is gone is shown as
broken rather than dropped, for the same reason 8.5 marks an unresolvable
wikilink.

8.12 — the launcher asks for the content language. Changing it afterwards
worked and ow init --language asked, but the launcher passed a hardcoded
"en", so a project created through the application was born in a language
nobody chose. It is a form rather than a chain of prompt() calls: Electron
does not implement window.prompt, and three named options are not something a
text box can offer. The list lives in one module both screens import, typed as
the Language union.

3.7 — the inbox watcher runs. watchInbox was built and tested with no
caller anywhere; 8.2's shell now holds it open for the life of a project window.

What the reviews changed

Wiring the watcher altered what opening a project does, and both reviewers
converged on it.

raw/ arrives with a git clone, so a repository can ship raw/_inbox/x.pdf
and the watcher as first wired ingested it during its initial scan: a stranger's
bytes parsed by pdfjs or mammoth in the privileged main process, and the file
deleted out of the user's working tree, with nobody having clicked anything. The
desktop now passes ingestExisting: false. What is already in the doorway is
listed and left alone; what arrives while the window is open is still taken on
sight, which is what the doorway is actually for.

Left alone is not lost — inboxWaiting/inboxDrain say what is there and take
it when asked. That report is pulled rather than pushed, which is the other
half: webContents.send before the document has loaded is dropped with no queue
and no error, so every outcome of the initial scan was being announced into the
void. Live arrivals stay pushed, buffered until did-finish-load.

Also closed: readManifest now checks the manifest's shape instead of casting
it (a non-string title reached the screen as a React child and blanked the
window, and this diff had just moved that render onto the primary view);
PageSources clears before fetching, so page A's provenance no longer sits
under page B's title; an unreadable source is told apart from an absent one;
return started.close() instead of void, which was escaping the chained
.catch; the inbox listener no longer bumps reloadKey, since 8.10's watcher
already reports and coalesces the same write.

Which plan

plans/open-wiki.md — tasks 3.7, 6.5, 8.12, with the notes updated to record
what was decided rather than what was hoped.

How it was verified

  • pnpm run typecheck, pnpm lint, pnpm format:check — clean.
  • pnpm test — 1079 passing, 1 skipped (a symlink case that now reports the
    skip when the OS refuses to create the link, instead of silently returning
    green as it did before).
  • pnpm test:coverage and scripts/ci/check-coverage.mjs per package — all
    above the 76% floor (access 94.49%, audio 99.46%, cli 96.70%, mcp 100%,
    desktop 92.93%).
  • npx @protonspy/scc validate — no findings.
  • code-review and security-review subagents on the diff; everything above
    under "what the reviews changed" is theirs.

Not verified: group 4's three manual audio checks remain outstanding — no
test in this repository has captured a frame or run ffmpeg, and nothing here
changes that. The new React components have no component tests either; this repo
has no such infrastructure, and coverage counts src/**/*.ts only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X24yWBFHyEkjZV5k8hDN4i

Summary by CodeRabbit

  • New Features
    • Added inbox monitoring with pending-file listings, explicit ingestion, and live arrival updates.
    • Added source citation panels with titles, links, and clear indicators for unavailable sources.
    • Replaced prompt-based project creation with an inline form supporting directory, name, and content-language selection.
  • Bug Fixes
    • Improved handling and reporting of inbox, source, and manifest errors.
    • Prevented stale citation results from appearing after page navigation.
  • Documentation
    • Clarified inbox watcher behavior and media checksum configuration.

protonspy and others added 2 commits August 1, 2026 18:08
…e doorway

Three tasks the plan ticked whose visible half was missing, and the small
untruths around them.

6.5 — a page now says which sources it came from, on the page itself. The
backend, its IPC channel and its tests all existed; no component called any of
them, so the feature was done from every angle except the user's. It carries the
title and somewhere to click rather than the bare id, and clicking opens the
panel a provenance link opens (8.6) at the source's own start — `p1` for a
document, `0:00` for a recording, which are the anchors `pdf.ts` and 4.13 write.
A citation whose source is gone is shown as broken rather than dropped, for the
same reason 8.5 marks an unresolvable wikilink: hiding it would leave the reader
believing the page is sourced.

8.12 — the launcher asks for the content language instead of passing a
hardcoded "en", so a project created through the application is no longer born
in a language nobody chose. It is a form rather than a chain of `prompt()`
calls: Electron does not implement `window.prompt`, and three named options are
not something a text box can offer. The list lives in one module both screens
import, typed as the `Language` union, so a language added to the setting and
forgotten in the picker is a compile error.

3.7 — the inbox watcher is wired into the shell, which is what 8.2 was always
meant to hold open. An arrival is reported where a drop is reported, because the
doorway and the drop zone are two ways into one registration. It starts
asynchronously — `watchInbox` waits for chokidar's initial scan — and a window
closed before the handle arrives closes it anyway. A doorway that stops working
is reported rather than logged: a quiet watcher is indistinguishable from an
empty inbox, and what it drops is material an agent believes it handed over.

Alongside them:

- `PUSH_CHANNELS` names the main-to-renderer channels once, so a push channel
  never gets an `ipcMain.handle` nobody wrote.
- The symlink and junction containment tests report a skip instead of returning
  green when the OS refuses to create the link. A junction needs no privilege on
  Windows, so failing to make one there is now a failure, not a skip — that is
  the whole reason the case exists beside the symlink one.
- `fetch-ffmpeg.mjs` no longer claims to pin a hash it does not pin. The URL is a
  rolling one, so the digest comes from the environment by design; the comment
  now says that instead of the opposite.
- `transcriptionProgress` is deleted. It had no callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X24yWBFHyEkjZV5k8hDN4i
The inbox wiring changed what opening a project does, and the reviews caught
it from two sides.

**`raw/` arrives with a clone.** A repository can ship `raw/_inbox/x.pdf`, and
the watcher as wired ingested it during its initial scan: a stranger's bytes
parsed by pdfjs or mammoth in the privileged main process, and the file deleted
out of the user's working tree, with nobody having clicked anything. The doorway
exists for an agent handing something over *during a session* — that is what an
event is — so the desktop now passes `ingestExisting: false`. What is already
there is listed and left alone; what arrives while the window is open is still
taken on sight.

Left alone is not lost: `inboxWaiting` and `inboxDrain` say what is in the
doorway and take it when asked. That report is pulled rather than pushed, which
is the other half of the fix — `webContents.send` before the document has loaded
is dropped with no queue and no error, so every outcome of the initial scan was
announced into the void. Live arrivals stay pushed and are now buffered until
`did-finish-load`.

Also from the reviews:

- `readManifest` checks the manifest's shape instead of casting it. A `title`
  that was not a string reached the screen as a React child and blanked the
  whole window — there is no error boundary — and this diff had just moved that
  render onto the primary reading view. The id comes from the directory now,
  never from the file, which is what `adr:0011` already said.
- `PageSources` clears before it fetches. It survives navigation, so page A's
  provenance sat under page B's title until the walk returned — the one wrong
  answer available to a component that says where a page came from.
- A source that is there but unreadable no longer reports as "there is no source
  named x". Different problems, different fixes.
- `return started.close()` rather than `void`: the discarded promise escaped the
  chained `.catch` and became an unhandled rejection in the main process.
- The inbox listener no longer bumps `reloadKey`. The ingest writes under `raw/`,
  which 8.10's watcher already reports and coalesces, where thirty arrivals here
  were thirty un-coalesced walks of the project.
- A drop appends to the banner instead of replacing it, so an inbox report the
  user has not dismissed is not discarded; a repeated identical outcome is not
  appended twice.

Tests: the fragment case now asserts through `locateCitation` rather than
against the constant it was transcribing — the recording branch is the one that
can silently break, and asserting `"0:00"` would have encoded a wrong string
faithfully. The channel test pins `PUSH_CHANNELS` membership, which it had
started trusting from the same place production trusts it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X24yWBFHyEkjZV5k8hDN4i
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds desktop inbox watching and draining, structured source citation results, manifest validation, and a language-aware project creation form. It also updates IPC routing, renderer state handling, access exports, filesystem tests, and FFmpeg extraction guidance.

Changes

Inbox ingestion

Layer / File(s) Summary
Inbox access and watcher controls
packages/access/src/sources/inbox.ts, packages/access/src/index.ts, packages/access/tests/sources-inbox.spec.ts
The access layer lists pending files, supports ingestExisting: false, and drains deferred files explicitly.
Desktop inbox pipeline
apps/desktop/src/main/*, apps/desktop/src/renderer/bridge.ts
Electron starts and closes the watcher, routes inbox IPC, buffers push notifications, supports stable fallback draining, and converts watcher results to drop outcomes.
Inbox renderer workflow
apps/desktop/src/renderer/App.tsx, apps/desktop/tests/sources.spec.ts
The renderer displays waiting files, drains them, appends outcomes, refreshes state, and deduplicates consecutive notifications.

Source citations and manifest validation

Layer / File(s) Summary
Manifest validation contract
packages/access/src/sources/manifest.ts, packages/access/src/index.ts, packages/access/src/read.ts, packages/access/tests/sources-manifest.spec.ts
Manifest parsing validates JSON shape, titles, kinds, and source identity through InvalidManifestError.
Structured source resolution
apps/desktop/src/main/sources.ts, apps/desktop/src/main/ipc.ts, apps/desktop/tests/sources.spec.ts
Page citations include titles, kinds, navigation fragments, and distinct missing or unreadable reasons.
Page source presentation
apps/desktop/src/renderer/App.tsx, apps/desktop/src/renderer/Panels.tsx, apps/desktop/src/renderer/bridge.ts, plans/open-wiki.md
The page view loads source details, renders broken citations, and opens resolved sources at valid fragments.

Language-aware project creation

Layer / File(s) Summary
Shared language configuration
apps/desktop/src/renderer/languages.ts, apps/desktop/src/renderer/Settings.tsx
Supported language values are centralized and reused by settings.
Inline project creation form
apps/desktop/src/renderer/Launcher.tsx, apps/desktop/src/renderer/tokens.css, plans/open-wiki.md
Project creation uses validated fields, language radio options, busy-state handling, and inline error reporting.

Validation and build documentation

Layer / File(s) Summary
Filesystem test portability
packages/access/tests/paths.spec.ts
Symlink and junction setup failures use explicit platform-aware skip or error handling.
FFmpeg download guidance
scripts/fetch-ffmpeg.mjs
The comments require an externally supplied digest, and extraction now targets only the FFmpeg executable.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InboxWatcher
  participant ElectronMain
  participant PreloadBridge
  participant RendererApp
  InboxWatcher->>ElectronMain: publish inbox outcome
  ElectronMain->>PreloadBridge: send buffered inbox event
  PreloadBridge->>RendererApp: deliver outcome
  RendererApp->>PreloadBridge: request inbox drain
  PreloadBridge->>ElectronMain: invoke inboxDrain
  ElectronMain->>InboxWatcher: drain pending files
  InboxWatcher-->>ElectronMain: return drop outcomes
  ElectronMain-->>RendererApp: return drained outcomes
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: closing plan gaps 3.7, 6.5, and 8.12, which matches the stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/plan-gaps

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
apps/desktop/src/renderer/languages.ts (1)

10-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make language coverage exhaustive.

Import the canonical LANGUAGES list from @open-wiki/access. Define labels as Record<Language, string> and generate the renderer options from that list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/languages.ts` around lines 10 - 17, Update the
renderer language options around the LANGUAGES constant to import and iterate
over the canonical LANGUAGES list from `@open-wiki/access`. Replace the array’s
inline language coverage with a Record<Language, string> label map, then
generate each option using the canonical value and its mapped label so all
supported languages are covered exhaustively.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/index.ts`:
- Around line 76-81: Update the inboxDrain helper so the active watcher branch
still invokes inbox.drain() but returns an empty outcome array, since the
watcher’s onOutcome handler already delivers those results to the renderer;
preserve drainInbox(root, ...) and its returned outcomes when no watcher exists.
Add a regression test covering a fake InboxControl whose drain both emits and
resolves outcomes, verifying the resulting report contains no duplicates.

In `@apps/desktop/src/renderer/App.tsx`:
- Around line 596-604: Update the InboxWaiting load flow around the load
callback and its useEffect to use a live flag like PageSources: mark the request
active before calling bridge().inboxWaiting(), ignore setNames updates after
cleanup, and deactivate the flag in the effect cleanup so stale responses cannot
overwrite newer results. Preserve the existing no-bridge guard and empty-list
error handling.

In `@apps/desktop/src/renderer/Launcher.tsx`:
- Around line 135-183: Update the new-project form in the Launcher component to
use a form onSubmit handler that triggers create, make the Create button a
submit button, and set the Cancel button to type="button". Disable the name and
directory inputs plus every language radio input while busy, preserving the
existing create and cancel behavior.

In `@apps/desktop/src/renderer/Panels.tsx`:
- Around line 68-76: Update the resolved-source anchor in the source rendering
branch of Panels to be keyboard-accessible by replacing it with a type="button"
button styled like a link, while preserving its title, displayed source.title,
and onOpen(source.id, source.fragment) behavior.

In `@packages/access/tests/paths.spec.ts`:
- Around line 63-72: In packages/access/tests/paths.spec.ts at lines 63-72, add
a shared capability-error check that allows ctx.skip only for symlink errors
with codes EPERM, EACCES, or ENOSYS; rethrow all other errors so they fail the
test. Apply the same classification at lines 87-97 while preserving the existing
Windows-specific failure behavior.

---

Nitpick comments:
In `@apps/desktop/src/renderer/languages.ts`:
- Around line 10-17: Update the renderer language options around the LANGUAGES
constant to import and iterate over the canonical LANGUAGES list from
`@open-wiki/access`. Replace the array’s inline language coverage with a
Record<Language, string> label map, then generate each option using the
canonical value and its mapped label so all supported languages are covered
exhaustively.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9df41e1b-9711-40a0-ad75-02635ded4863

📥 Commits

Reviewing files that changed from the base of the PR and between e583583 and 589eb0b.

📒 Files selected for processing (24)
  • apps/desktop/src/main/channels.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/ingest.ts
  • apps/desktop/src/main/ipc.ts
  • apps/desktop/src/main/preload.ts
  • apps/desktop/src/main/sources.ts
  • apps/desktop/src/main/transcribe-run.ts
  • apps/desktop/src/renderer/App.tsx
  • apps/desktop/src/renderer/Launcher.tsx
  • apps/desktop/src/renderer/Panels.tsx
  • apps/desktop/src/renderer/Settings.tsx
  • apps/desktop/src/renderer/bridge.ts
  • apps/desktop/src/renderer/languages.ts
  • apps/desktop/src/renderer/tokens.css
  • apps/desktop/tests/sources.spec.ts
  • packages/access/src/index.ts
  • packages/access/src/read.ts
  • packages/access/src/sources/inbox.ts
  • packages/access/src/sources/manifest.ts
  • packages/access/tests/paths.spec.ts
  • packages/access/tests/sources-inbox.spec.ts
  • packages/access/tests/sources-manifest.spec.ts
  • plans/open-wiki.md
  • scripts/fetch-ffmpeg.mjs
💤 Files with no reviewable changes (1)
  • apps/desktop/src/main/transcribe-run.ts

Comment on lines +76 to +81
// The window's own watcher once its initial scan has finished, so an explicit
// drain and an event cannot both read the same file and both try to register
// the same id. Standalone until then: a drain must still work in the seconds
// between the window opening and the scan completing.
const inboxDrain = (root: string): Promise<InboxOutcome[]> =>
inbox ? inbox.drain() : drainInbox(root, { stabilityMs: INBOX_STABILITY_MS });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Explicit inbox drain reports every outcome twice to the renderer.

inboxDrain returns inbox.drain()'s result. inbox.drain() (the InboxWatcher from packages/access/src/sources/inbox.ts) internally calls announce(outcome) for every outcome and returns the same outcomes. announce invokes the onOutcome handler registered at line 151, which pushes the outcome to the renderer over CHANNELS.inbox. The renderer already appends every CHANNELS.inbox push through its onInbox subscription (apps/desktop/src/renderer/App.tsx), then separately appends the array returned by the inboxDrain() IPC call through the onTaken prop (apps/desktop/src/renderer/App.tsx), which does not deduplicate.

The result: clicking "Add them" while the watcher is active shows each newly ingested or refused file twice in the "X of Y added" report. This only affects the explicit on-request drain; live add/change events are reported once, as expected.

Return an empty array from the watcher branch of this helper, since those outcomes are already delivered through onOutcome.

🐛 Proposed fix to stop the double delivery
   const inboxDrain = (root: string): Promise<InboxOutcome[]> =>
-    inbox ? inbox.drain() : drainInbox(root, { stabilityMs: INBOX_STABILITY_MS });
+    inbox
+      ? // `inbox.drain()` already reports every outcome through `onOutcome`
+        // (`CHANNELS.inbox`) below. Returning them here too would hand the
+        // renderer the same outcomes a second time.
+        inbox.drain().then(() => [])
+      : drainInbox(root, { stabilityMs: INBOX_STABILITY_MS });

Consider adding a regression test in apps/desktop/tests/sources.spec.ts that wires a fake InboxControl whose drain() both resolves outcomes and simulates the onOutcome push, then asserts the resulting drop report has no duplicate entries — the current tests only exercise inboxDrain() without a live deps.inbox, so this path is untested.

Also applies to: 147-169

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/index.ts` around lines 76 - 81, Update the inboxDrain
helper so the active watcher branch still invokes inbox.drain() but returns an
empty outcome array, since the watcher’s onOutcome handler already delivers
those results to the renderer; preserve drainInbox(root, ...) and its returned
outcomes when no watcher exists. Add a regression test covering a fake
InboxControl whose drain both emits and resolves outcomes, verifying the
resulting report contains no duplicates.

Comment on lines +596 to +604
const load = useCallback(() => {
if (!hasBridge()) return;
void bridge()
.inboxWaiting()
.then(setNames)
.catch(() => setNames([]));
}, []);

useEffect(load, [load, reloadKey]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

InboxWaiting.load() has no guard against out-of-order responses.

load() calls bridge().inboxWaiting() on every reloadKey change with no protection against a slower, earlier response overwriting a later one's result. reloadKey changes often in this component tree (drops, inbox drains, coalesced project-change events), so overlapping calls are plausible.

The sibling PageSources component in apps/desktop/src/renderer/Panels.tsx guards its own reloadKey-driven fetch with a live flag for the same reason. Apply the same pattern here.

♻️ Proposed fix to ignore stale responses
   const load = useCallback(() => {
     if (!hasBridge()) return;
-    void bridge()
-      .inboxWaiting()
-      .then(setNames)
-      .catch(() => setNames([]));
+    let live = true;
+    void bridge()
+      .inboxWaiting()
+      .then((found) => {
+        if (live) setNames(found);
+      })
+      .catch(() => {
+        if (live) setNames([]);
+      });
+    return () => {
+      live = false;
+    };
   }, []);
 
-  useEffect(load, [load, reloadKey]);
+  useEffect(() => load(), [load, reloadKey]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const load = useCallback(() => {
if (!hasBridge()) return;
void bridge()
.inboxWaiting()
.then(setNames)
.catch(() => setNames([]));
}, []);
useEffect(load, [load, reloadKey]);
const load = useCallback(() => {
if (!hasBridge()) return;
let live = true;
void bridge()
.inboxWaiting()
.then((found) => {
if (live) setNames(found);
})
.catch(() => {
if (live) setNames([]);
});
return () => {
live = false;
};
}, []);
useEffect(() => load(), [load, reloadKey]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/App.tsx` around lines 596 - 604, Update the
InboxWaiting load flow around the load callback and its useEffect to use a live
flag like PageSources: mark the request active before calling
bridge().inboxWaiting(), ignore setNames updates after cleanup, and deactivate
the flag in the effect cleanup so stale responses cannot overwrite newer
results. Preserve the existing no-bridge guard and empty-list error handling.

Comment on lines +135 to +183
return (
<section className="launcher__new">
<h3>New project</h3>
<label>
Name
<input
className="editor__source"
value={name}
placeholder="fenix"
autoFocus
onChange={(event) => setName(event.target.value)}
/>
</label>
<label>
Directory
<input
className="editor__source"
value={directory}
placeholder="C:\projects\fenix"
onChange={(event) => setDirectory(event.target.value)}
/>
</label>
<fieldset className="launcher__languages">
<legend>Content language</legend>
<p className="empty">
What transcription is told to expect, and what the generated CLAUDE.md tells the agent to
write pages in. The schema itself stays English, and this is changeable later.
</p>
<div className="editor__bar">
{LANGUAGES.map((option) => (
<label key={option.value}>
<input
type="radio"
name="language"
checked={language === option.value}
onChange={() => setLanguage(option.value)}
/>{" "}
{option.label}
</label>
))}
</div>
</fieldset>
<div className="editor__bar">
<button onClick={() => void create()} disabled={busy}>
{busy ? "Creating…" : "Create"}
</button>
<button onClick={onCancel} disabled={busy}>
Cancel
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use form submission and disable all input controls while creating.

Because this wrapper is a section, Enter in a text input does not start creation. Use a <form onSubmit> with a submit button and a type="button" cancel button.

Add disabled={busy} to both text inputs and each language radio input. Otherwise, the displayed values can change while createProject uses the earlier values.

Proposed fix
-    <section className="launcher__new">
+    <form
+      className="launcher__new"
+      onSubmit={(event) => {
+        event.preventDefault();
+        void create();
+      }}
+    >
...
         <input
           className="editor__source"
           value={name}
+          disabled={busy}
...
         <input
           className="editor__source"
           value={directory}
+          disabled={busy}
...
                 name="language"
                 checked={language === option.value}
+                disabled={busy}
                 onChange={() => setLanguage(option.value)}
...
-        <button onClick={() => void create()} disabled={busy}>
+        <button type="submit" disabled={busy}>
...
-        <button onClick={onCancel} disabled={busy}>
+        <button type="button" onClick={onCancel} disabled={busy}>
...
-    </section>
+    </form>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<section className="launcher__new">
<h3>New project</h3>
<label>
Name
<input
className="editor__source"
value={name}
placeholder="fenix"
autoFocus
onChange={(event) => setName(event.target.value)}
/>
</label>
<label>
Directory
<input
className="editor__source"
value={directory}
placeholder="C:\projects\fenix"
onChange={(event) => setDirectory(event.target.value)}
/>
</label>
<fieldset className="launcher__languages">
<legend>Content language</legend>
<p className="empty">
What transcription is told to expect, and what the generated CLAUDE.md tells the agent to
write pages in. The schema itself stays English, and this is changeable later.
</p>
<div className="editor__bar">
{LANGUAGES.map((option) => (
<label key={option.value}>
<input
type="radio"
name="language"
checked={language === option.value}
onChange={() => setLanguage(option.value)}
/>{" "}
{option.label}
</label>
))}
</div>
</fieldset>
<div className="editor__bar">
<button onClick={() => void create()} disabled={busy}>
{busy ? "Creating…" : "Create"}
</button>
<button onClick={onCancel} disabled={busy}>
Cancel
</button>
return (
<form
className="launcher__new"
onSubmit={(event) => {
event.preventDefault();
void create();
}}
>
<h3>New project</h3>
<label>
Name
<input
className="editor__source"
value={name}
disabled={busy}
placeholder="fenix"
autoFocus
onChange={(event) => setName(event.target.value)}
/>
</label>
<label>
Directory
<input
className="editor__source"
value={directory}
disabled={busy}
placeholder="C:\projects\fenix"
onChange={(event) => setDirectory(event.target.value)}
/>
</label>
<fieldset className="launcher__languages">
<legend>Content language</legend>
<p className="empty">
What transcription is told to expect, and what the generated CLAUDE.md tells the agent to
write pages in. The schema itself stays English, and this is changeable later.
</p>
<div className="editor__bar">
{LANGUAGES.map((option) => (
<label key={option.value}>
<input
type="radio"
name="language"
checked={language === option.value}
disabled={busy}
onChange={() => setLanguage(option.value)}
/>{" "}
{option.label}
</label>
))}
</div>
</fieldset>
<div className="editor__bar">
<button type="submit" disabled={busy}>
{busy ? "Creating…" : "Create"}
</button>
<button type="button" onClick={onCancel} disabled={busy}>
Cancel
</button>
</div>
</form>
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/Launcher.tsx` around lines 135 - 183, Update the
new-project form in the Launcher component to use a form onSubmit handler that
triggers create, make the Create button a submit button, and set the Cancel
button to type="button". Disable the name and directory inputs plus every
language radio input while busy, preserving the existing create and cancel
behavior.

Comment on lines +68 to +76
{source.kind === null ? (
<span className="wikilink--broken" title={source.reason ?? source.id}>
{source.id}
</span>
) : (
<a title={source.id} onClick={() => onOpen(source.id, source.fragment)}>
{source.title}
</a>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- file outline ---'
ast-grep outline apps/desktop/src/renderer/Panels.tsx --match 'SourceAt' --view expanded || true
printf '%s\n' '--- relevant source ---'
sed -n '1,130p' apps/desktop/src/renderer/Panels.tsx
printf '%s\n' '--- anchor/button styles and usages ---'
rg -n -C 3 'link-button|wikilink--broken|SourceAt|onOpen=\{|<a[^>]*onClick' apps/desktop/src/renderer apps/desktop/src -g '*.{css,scss,tsx,ts}' || true

Repository: protonspy/open-wiki

Length of output: 15280


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- remaining component and styles ---'
sed -n '180,245p' apps/desktop/src/renderer/Panels.tsx
sed -n '250,310p' apps/desktop/src/renderer/tokens.css
printf '%s\n' '--- accessibility and link-button conventions ---'
rg -n -C 4 'tabIndex|onKeyDown|role="link"|role=.link.|button.*className|className=.*button|jsx-a11y|accessib' apps/desktop package.json -g '*.{tsx,ts,css,json}' || true
printf '%s\n' '--- deterministic JSX check ---'
python3 - <<'PY'
from pathlib import Path
text = Path("apps/desktop/src/renderer/Panels.tsx").read_text()
start = text.index('<a title={source.id}')
end = text.index('</a>', start) + len('</a>')
element = text[start:end]
print(element)
for attr in ("href", "tabIndex", "onKeyDown", "role"):
    print(f"{attr}: {attr in element}")
PY

Repository: protonspy/open-wiki

Length of output: 5541


Make the resolved source link keyboard-accessible.

The <a> has no href, tabIndex, role, or keyboard handler. Replace it with a <button type="button"> styled as a link, or add complete keyboard link behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/Panels.tsx` around lines 68 - 76, Update the
resolved-source anchor in the source rendering branch of Panels to be
keyboard-accessible by replacing it with a type="button" button styled like a
link, while preserving its title, displayed source.title, and onOpen(source.id,
source.fragment) behavior.

Comment on lines 63 to +72
try {
symlinkSync(outside, link);
} catch {
// Some Windows accounts lack the symlink privilege; that is a different
// failure from the containment logic and should not fail this test.
rmSync(root, { recursive: true, force: true });
} catch (err) {
// Creating a symlink is privileged on most Windows accounts, and that is
// a different failure from the containment logic. **Reported as a skip,
// never as a pass**: a test that silently returns green is one nobody
// knows stopped running, and this is the check standing between a
// citation and a file anywhere on disk.
rmSync(outside, { force: true });
ctx.skip(`symlink creation unavailable: ${err instanceof Error ? err.message : err}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not convert arbitrary filesystem errors into skipped tests.

Use one shared capability-error check. Skip only EPERM, EACCES, or ENOSYS. Keep all other errors as test failures.

  • packages/access/tests/paths.spec.ts#L63-L72: classify symlink errors before calling ctx.skip.
  • packages/access/tests/paths.spec.ts#L87-L97: preserve the Windows failure behavior and apply the same classification on other platforms.
📍 Affects 1 file
  • packages/access/tests/paths.spec.ts#L63-L72 (this comment)
  • packages/access/tests/paths.spec.ts#L87-L97
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/access/tests/paths.spec.ts` around lines 63 - 72, In
packages/access/tests/paths.spec.ts at lines 63-72, add a shared
capability-error check that allows ctx.skip only for symlink errors with codes
EPERM, EACCES, or ENOSYS; rethrow all other errors so they fail the test. Apply
the same classification at lines 87-97 while preserving the existing
Windows-specific failure behavior.

The release would have failed at "Fetch and verify ffmpeg", with the hash set
and everything else correct.

The archive is `ffmpeg-<version>-essentials_build/bin/ffmpeg.exe`, so stripping
one leading component left `bin/ffmpeg.exe` and the script's own check —
`extraction produced no vendor/ffmpeg/ffmpeg.exe` — was right to fail. Nothing
had ever run it against a real download: `vendor/ffmpeg/` is gitignored, no test
covers a 110 MB fetch, and CI only reaches this step on a `v*` tag, which this
repository has never had.

Naming the member is also what makes the docstring true. It has always claimed
to extract "only `ffmpeg.exe`", where it was unpacking the documentation and the
presets as well.

Verified by running it: `node scripts/fetch-ffmpeg.mjs` against the real URL
with the real digest now reports `ok → vendor\ffmpeg\ffmpeg.exe`, and that
binary answers `ffmpeg version 8.1.2-essentials_build`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X24yWBFHyEkjZV5k8hDN4i

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/fetch-ffmpeg.mjs (1)

84-89: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the missing-hash guidance match the external configuration contract.

FFMPEG_SHA256 is supplied through the environment. The current message also tells maintainers to pin the hash in this script. Use environment-only wording.

Proposed wording
-      "FFMPEG_SHA256 is not set. Pin the expected hash of FFMPEG_URL (in this " +
-        "script or the FFMPEG_SHA256 env var) before fetching — a download " +
-        "that verifies nothing is the fetch-and-execute this product refuses.",
+      "FFMPEG_SHA256 is not set. Set the expected SHA-256 for FFMPEG_URL in " +
+        "the FFMPEG_SHA256 environment variable before fetching. The product " +
+        "refuses to bundle an unverified download.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch-ffmpeg.mjs` around lines 84 - 89, Update the
missing-FFMPEG_SHA256 message in the fetch validation block to state that the
expected hash must be set through the FFMPEG_SHA256 environment variable only;
remove the suggestion to pin it in the script while preserving the existing
failure behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/fetch-ffmpeg.mjs`:
- Around line 74-78: Update the extraction command in the spawnSync call to
invoke bsdtar explicitly instead of tar, while preserving the existing ZIP
arguments, destination, member pattern, and inherited stdio behavior.

---

Outside diff comments:
In `@scripts/fetch-ffmpeg.mjs`:
- Around line 84-89: Update the missing-FFMPEG_SHA256 message in the fetch
validation block to state that the expected hash must be set through the
FFMPEG_SHA256 environment variable only; remove the suggestion to pin it in the
script while preserving the existing failure behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e22290b1-7eb6-46d4-915f-5365fb74a21f

📥 Commits

Reviewing files that changed from the base of the PR and between 589eb0b and 92ac22a.

📒 Files selected for processing (1)
  • scripts/fetch-ffmpeg.mjs

Comment thread scripts/fetch-ffmpeg.mjs
Comment on lines +74 to +78
const result = spawnSync(
"tar",
["-xf", zip, "-C", dest, "--strip-components=2", "*/bin/ffmpeg.exe"],
{ stdio: "inherit" },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Find callers and runner/tool assumptions.
rg -n -C 6 'fetch-ffmpeg\.mjs|runs-on:|shell:|FFMPEG_URL|FFMPEG_SHA256' \
  .github/workflows/release.yml scripts || true

# Identify the local tar implementation.
if command -v tar >/dev/null 2>&1; then
  tar --version 2>&1 || true
fi

Repository: protonspy/open-wiki

Length of output: 15697


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the extraction code and workflow context, then check repository
# documentation for platform and tool assumptions.
sed -n '65,82p' scripts/fetch-ffmpeg.mjs
sed -n '14,25p;90,112p' .github/workflows/release.yml
rg -n -i 'bsdtar|gnu tar|windows-latest|ffmpeg-release-essentials|strip-components|wildcard' \
  README.md docs scripts .github 2>/dev/null || true

Repository: protonspy/open-wiki

Length of output: 3418


🌐 Web query:

GitHub Actions windows-latest runner tar implementation bsdtar GNU tar Windows image

💡 Result:

GitHub Actions Windows runners, including the windows-latest image, contain both BSD tar and GNU tar by default, but the GitHub-maintained actions/cache tooling uses specific logic to select between them [1][2]. BSD tar is natively available on Windows (typically at C:\Windows\System32\tar.exe) [3][1]. GNU tar is also available on these images, typically provided by Git for Windows (e.g., at C:\Program Files\Git\usr\bin\tar.exe) [1][4][2]. The GitHub actions/cache implementation has evolved in how it handles these tools: 1. Historical context: Early versions of the cache action explicitly used the system BSD tar because it was reliable and present on Windows [5]. However, this caused various issues, such as incompatibility with zstd compression and handling of certain file structures [6][1]. 2. Current implementation: The cache action logic is designed to prioritize the use of GNU tar for better compatibility and performance, particularly with zstd [6][7]. The implementation attempts to detect the presence and path of GNU tar; if detected, it uses that version with necessary flags (like --force-local) to ensure compatibility with Windows file paths [3][8]. 3. Fallback mechanism: If GNU tar is not found or is unavailable, the action falls back to BSD tar [6][7]. This ensures that the action remains functional across different runner environments, including self-hosted runners where GNU tar might not be installed in the standard locations found on GitHub-hosted runners [3][9]. In summary, while both tools exist on the Windows runner image, the GitHub cache action dynamically selects the tool based on availability and capability, with a preference for GNU tar [6][3]. Users writing their own workflows can explicitly call either binary by providing the full path (e.g., C:\Program Files\Git\usr\bin\tar.exe for GNU tar) if they have specific requirements [4].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '65,82p' scripts/fetch-ffmpeg.mjs
sed -n '14,25p;90,112p' .github/workflows/release.yml
rg -n -i 'bsdtar|gnu tar|windows-latest|ffmpeg-release-essentials|strip-components|wildcard' \
  README.md docs scripts .github 2>/dev/null || true

Repository: protonspy/open-wiki

Length of output: 3418


🌐 Web query:

site:github.com/actions/runner-images windows-latest tar.exe PATH C:\\Windows\\System32 C:\\Program Files\\Git\\usr\\bin

💡 Result:

On GitHub Actions windows-latest runners, the availability and priority of tar.exe has been a long-standing point of conflict due to the presence of multiple implementations on the system [1][2][3]. The two primary locations are: 1. C:\Windows\System32\tar.exe: This is the native Windows implementation (bsdtar). It is generally preferred by system-level tools but has known limitations, such as difficulties handling certain .tar.xz archives or specific symlink configurations [1][4][5]. 2. C:\Program Files\Git\usr\bin\tar.exe: This is the GNU tar implementation bundled with Git for Windows [4][3]. It is often more compatible with Linux-style workflows and complex archives but can encounter path resolution issues when called from non-Git shells, or cause regressions if it inadvertently overrides the system default [1][4]. Key Technical Context: - Path Conflicts: Because both directories are typically included in the PATH environment variable, the version of tar executed depends on the order of the PATH entries at runtime [1][6]. GitHub Actions runner images have undergone several configuration changes over time to balance these conflicts, often leading to inconsistent behavior where PATH order may shift or differ across image versions [1][7][6]. - Workarounds: If your workflow requires a specific version of tar, it is recommended to explicitly call the binary by its full path [4]. For example, if you require the Git-provided GNU tar to avoid issues with the system-native version, you can invoke it directly: & "C:\Program Files\Git\usr\bin\tar.exe" -xzvf archive.tar.gz [4] - PATH Modification: If you must ensure a specific tar is used by default throughout a job, you can modify the PATH for subsequent steps by prepending the preferred directory to the GITHUB_PATH environment variable [5][8]: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH Due to the sensitive nature of the runner environment's default PATH configuration, modifying these system variables can sometimes lead to unexpected behavior in other tools [7][6][9]. Explicitly invoking the required executable by its absolute path remains the most reliable practice for GitHub Actions [4].

Citations:


🌐 Web query:

bsdtar extract archive member wildcard command line pattern default --wildcards

💡 Result:

In bsdtar, wildcard pattern matching is enabled by default when extracting or listing archive members [1][2][3]. When you provide a filename or pattern on the command line, bsdtar interprets shell-style globbing patterns (as documented in tcsh(1)) to identify which items in the archive to process [2][3]. There is no dedicated --wildcards or --no-wildcards command-line option in bsdtar to toggle this behavior [4]. Because wildcards are always active, if you need to extract a file that contains special shell characters (such as an asterisk ''), you must escape those characters, for example by using backslashes ('*') or enclosing them in brackets ('[]') [4]. If you are reading filenames from a file (using the -T or --files-from option), bsdtar continues to treat those entries as patterns, which can cause unexpected behavior if the file list contains literal filenames that happen to include wildcard characters [4]. In such cases, you must manually escape any characters that have special meaning to the globbing engine before passing them to bsdtar [4].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the exact tar invocation and all workflow steps that can invoke the
# script. Check whether the repository pins a tar executable or PATH.
rg -n -C 4 'spawnSync\(\s*"tar"|tar\.exe|PATH|fetch-ffmpeg\.mjs|runs-on:' \
  scripts .github

Repository: protonspy/open-wiki

Length of output: 8178


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 4 'spawnSync\(\s*"tar"|tar\.exe|PATH|fetch-ffmpeg\.mjs|runs-on:' \
  scripts .github

Repository: protonspy/open-wiki

Length of output: 8178


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Probe the local GNU tar with an in-memory-created ZIP archive in a temporary
# directory. This does not execute repository code or alter repository files.
python3 - <<'PY'
import pathlib
import subprocess
import tempfile
import zipfile

with tempfile.TemporaryDirectory() as td:
    root = pathlib.Path(td)
    archive = root / "ffmpeg.zip"
    destination = root / "out"
    destination.mkdir()

    with zipfile.ZipFile(archive, "w") as zf:
        zf.writestr("ffmpeg-7.0-essentials_build/bin/ffmpeg.exe", b"ok")

    for args in (
        ["-xf", str(archive), "-C", str(destination),
         "--strip-components=2", "*/bin/ffmpeg.exe"],
        ["-xf", str(archive), "-C", str(destination),
         "--wildcards", "--strip-components=2", "*/bin/ffmpeg.exe"],
    ):
        result = subprocess.run(
            ["tar", *args], text=True, capture_output=True
        )
        print("args:", args[2:])
        print("status:", result.returncode)
        print("stderr:", result.stderr.strip())
        print("output:", sorted(str(p.relative_to(destination)) for p in destination.rglob("*")))
        for child in destination.iterdir():
            if child.is_dir():
                for p in child.rglob("*"):
                    if p.is_file():
                        p.unlink()
                child.rmdir()
            else:
                child.unlink()
PY

Repository: protonspy/open-wiki

Length of output: 678


🌐 Web query:

GNU tar support ZIP archives tar -xf .zip official manual

💡 Result:

GNU tar does not natively support ZIP archives [1][2]. It is designed specifically to handle tar archives and various compression formats (such as gzip, bzip2, xz, etc.) used in conjunction with tar [1][2]. If you are using the tar command and successfully able to manipulate ZIP files, you are likely using bsdtar (part of the libarchive project), which is often aliased to tar on some operating systems like macOS and certain BSD-based distributions [3][4][5][6]. Unlike GNU tar, bsdtar supports a wide variety of archive formats, including ZIP, 7-zip, and JAR [3][4][5]. To verify which version of tar you have installed, you can run the following command in your terminal: tar --version If the output mentions "GNU tar," it will not support ZIP files [1][2]. If it mentions "bsdtar" or "libarchive," it will support ZIP files [4][5].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
import pathlib
import subprocess
import tempfile
import zipfile

with tempfile.TemporaryDirectory() as td:
    root = pathlib.Path(td)
    archive = root / "ffmpeg.zip"
    destination = root / "out"
    destination.mkdir()

    with zipfile.ZipFile(archive, "w") as zf:
        zf.writestr("ffmpeg-7.0-essentials_build/bin/ffmpeg.exe", b"ok")

    for args in (
        ["-xf", str(archive), "-C", str(destination),
         "--strip-components=2", "*/bin/ffmpeg.exe"],
        ["-xf", str(archive), "-C", str(destination),
         "--wildcards", "--strip-components=2", "*/bin/ffmpeg.exe"],
    ):
        result = subprocess.run(["tar", *args], text=True, capture_output=True)
        print("args:", args[2:])
        print("status:", result.returncode)
        print("stderr:", result.stderr.strip())
        print("output:", sorted(
            str(p.relative_to(destination)) for p in destination.rglob("*")
        ))
        for p in sorted(destination.rglob("*"), reverse=True):
            if p.is_file():
                p.unlink()
            elif p.is_dir():
                p.rmdir()
PY

Repository: protonspy/open-wiki

Length of output: 678


Invoke bsdtar explicitly for ZIP extraction.

windows-latest includes both bsdtar and GNU tar. spawnSync("tar", ...) depends on PATH. GNU tar cannot read the ZIP archive, so extraction fails before the member pattern is evaluated. Invoke bsdtar explicitly or use a ZIP-specific extractor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch-ffmpeg.mjs` around lines 74 - 78, Update the extraction command
in the spawnSync call to invoke bsdtar explicitly instead of tar, while
preserving the existing ZIP arguments, destination, member pattern, and
inherited stdio behavior.

@protonspy
protonspy merged commit 0cd208b into main Aug 1, 2026
11 checks passed
@protonspy
protonspy deleted the fix/plan-gaps branch August 1, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant